From da809a9fc52961d968035768b5253cd982dd1cc7 Mon Sep 17 00:00:00 2001 From: jensp Date: Wed, 23 Sep 2015 16:51:25 +0000 Subject: [PATCH] CCM NG: - Fixed some errors - Ported basic servlets and Bebop classes from old code git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@3646 8810af33-2d31-482b-a856-94f89814c4df --- ccm-core/pom.xml | 13 + .../bebop/AbstractSingleSelectionModel.java | 99 ++ .../java/com/arsdigita/bebop/BaseLink.java | 545 +++++++ .../java/com/arsdigita/bebop/BasePage.java | 52 + .../main/java/com/arsdigita/bebop/Bebop.java | 40 + .../java/com/arsdigita/bebop/BebopConfig.java | 191 +++ .../java/com/arsdigita/bebop/BoxPanel.java | 168 +++ .../java/com/arsdigita/bebop/ColumnPanel.java | 574 +++++++ .../java/com/arsdigita/bebop/Completable.java | 69 + .../java/com/arsdigita/bebop/Component.java | 387 +++++ .../java/com/arsdigita/bebop/ConfirmPage.java | 198 +++ .../java/com/arsdigita/bebop/Container.java | 123 ++ .../java/com/arsdigita/bebop/ControlLink.java | 191 +++ .../arsdigita/bebop/DescriptiveComponent.java | 117 ++ .../main/java/com/arsdigita/bebop/Form.java | 558 +++++++ .../java/com/arsdigita/bebop/FormData.java | 832 ++++++++++ .../java/com/arsdigita/bebop/FormModel.java | 550 +++++++ .../arsdigita/bebop/FormProcessException.java | 181 +++ .../java/com/arsdigita/bebop/FormSection.java | 777 ++++++++++ .../java/com/arsdigita/bebop/FormStep.java | 187 +++ .../java/com/arsdigita/bebop/GridPanel.java | 344 +++++ .../main/java/com/arsdigita/bebop/Label.java | 447 ++++++ .../main/java/com/arsdigita/bebop/List.java | 821 ++++++++++ .../main/java/com/arsdigita/bebop/Page.java | 1344 +++++++++++++++++ .../com/arsdigita/bebop/PageErrorDisplay.java | 173 +++ .../java/com/arsdigita/bebop/PageState.java | 1075 +++++++++++++ .../bebop/ParameterSingleSelectionModel.java | 106 ++ .../com/arsdigita/bebop/RequestLocal.java | 142 ++ .../bebop/SessionExpiredException.java | 30 + .../com/arsdigita/bebop/SimpleComponent.java | 361 +++++ .../com/arsdigita/bebop/SimpleContainer.java | 268 ++++ .../arsdigita/bebop/SingleSelectionModel.java | 111 ++ .../arsdigita/bebop/event/ActionEvent.java | 51 + .../arsdigita/bebop/event/ActionListener.java | 45 + .../arsdigita/bebop/event/ChangeEvent.java | 33 + .../arsdigita/bebop/event/ChangeListener.java | 30 + .../bebop/event/EventListenerList.java | 121 ++ .../bebop/event/FormCancelListener.java | 47 + .../bebop/event/FormInitListener.java | 52 + .../bebop/event/FormProcessListener.java | 69 + .../bebop/event/FormSectionEvent.java | 65 + .../bebop/event/FormSubmissionListener.java | 49 + .../bebop/event/FormValidationListener.java | 62 + .../com/arsdigita/bebop/event/PageEvent.java | 57 + .../arsdigita/bebop/event/ParameterEvent.java | 75 + .../bebop/event/ParameterListener.java | 43 + .../com/arsdigita/bebop/event/PrintEvent.java | 63 + .../arsdigita/bebop/event/PrintListener.java | 79 + .../arsdigita/bebop/event/RequestEvent.java | 49 + .../bebop/event/RequestListener.java | 46 + .../bebop/event/SearchAndSelectListener.java | 38 + .../bebop/event/SearchAndSelectModel.java | 71 + .../bebop/event/TableActionAdapter.java | 52 + .../bebop/event/TableActionEvent.java | 83 + .../bebop/event/TableActionListener.java | 51 + .../bebop/event/TreeExpansionEvent.java | 50 + .../bebop/event/TreeExpansionListener.java | 42 + .../arsdigita/bebop/form/CheckboxGroup.java | 68 + .../com/arsdigita/bebop/form/DHTMLEditor.java | 321 ++++ .../java/com/arsdigita/bebop/form/Date.java | 487 ++++++ .../com/arsdigita/bebop/form/DateTime.java | 150 ++ .../com/arsdigita/bebop/form/Deditor.java | 174 +++ .../com/arsdigita/bebop/form/Fieldset.java | 36 + .../com/arsdigita/bebop/form/FileUpload.java | 108 ++ .../bebop/form/FormErrorDisplay.java | 97 ++ .../java/com/arsdigita/bebop/form/Hidden.java | 53 + .../com/arsdigita/bebop/form/ImageSubmit.java | 136 ++ .../arsdigita/bebop/form/MultipleSelect.java | 53 + .../bebop/form/MultipleSelectPairWidget.java | 561 +++++++ .../java/com/arsdigita/bebop/form/Option.java | 317 ++++ .../com/arsdigita/bebop/form/OptionGroup.java | 574 +++++++ .../com/arsdigita/bebop/form/Password.java | 80 + .../com/arsdigita/bebop/form/RadioGroup.java | 119 ++ .../java/com/arsdigita/bebop/form/Reset.java | 58 + .../arsdigita/bebop/form/SearchAndSelect.java | 484 ++++++ .../java/com/arsdigita/bebop/form/Select.java | 86 ++ .../arsdigita/bebop/form/SingleSelect.java | 88 ++ .../java/com/arsdigita/bebop/form/Submit.java | 274 ++++ .../com/arsdigita/bebop/form/TextArea.java | 194 +++ .../bebop/form/TextEntryFormSection.java | 79 + .../com/arsdigita/bebop/form/TextField.java | 73 + .../java/com/arsdigita/bebop/form/Time.java | 362 +++++ .../java/com/arsdigita/bebop/form/Widget.java | 793 ++++++++++ .../bebop/list/AbstractListModelBuilder.java | 37 + .../bebop/list/DefaultListCellRenderer.java | 54 + .../bebop/list/ListCellRenderer.java | 77 + .../com/arsdigita/bebop/list/ListModel.java | 108 ++ .../bebop/list/ListModelBuilder.java | 57 + .../arsdigita/bebop/page/PageTransformer.java | 612 ++++++++ .../bebop/parameters/ArrayParameter.java | 362 +++++ .../bebop/parameters/BigDecimalParameter.java | 60 + .../bebop/parameters/BigIntegerParameter.java | 58 + .../bebop/parameters/BitSetParameter.java | 281 ++++ .../bebop/parameters/BooleanParameter.java | 49 + .../CancellableValidationListener.java | 80 + .../DateInRangeValidationListener.java | 114 ++ .../bebop/parameters/DateParameter.java | 121 ++ .../bebop/parameters/DateTimeParameter.java | 136 ++ .../bebop/parameters/DoubleParameter.java | 53 + .../bebop/parameters/EmailParameter.java | 125 ++ .../parameters/EmailValidationListener.java | 44 + .../parameters/EnumerationParameter.java | 39 + .../EnumerationValidationListener.java | 114 ++ .../FileSizeValidationListener.java | 83 + .../bebop/parameters/FloatParameter.java | 52 + .../parameters/FloatValidationListener.java | 44 + .../GlobalizedParameterListener.java | 67 + .../HTMLColourCodeValidationListener.java | 43 + .../parameters/IncompleteDateParameter.java | 147 ++ .../bebop/parameters/IntegerParameter.java | 52 + .../parameters/IntegerValidationListener.java | 44 + .../parameters/KeywordValidationListener.java | 77 + .../bebop/parameters/LongParameter.java | 50 + .../NotEmptyValidationListener.java | 105 ++ .../parameters/NotNullValidationListener.java | 83 + .../NotWhiteSpaceValidationListener.java | 42 + .../NumberInRangeValidationListener.java | 136 ++ .../bebop/parameters/NumberParameter.java | 65 + .../bebop/parameters/ParameterData.java | 331 ++++ .../bebop/parameters/ParameterModel.java | 498 ++++++ .../parameters/ParameterModelWrapper.java | 122 ++ .../SingleLineValidationListener.java | 44 + .../StringInRangeValidationListener.java | 124 ++ ...ngIsLettersOrDigitsValidationListener.java | 87 ++ .../StringLengthValidationListener.java | 96 ++ .../bebop/parameters/StringParameter.java | 68 + .../TidyHTMLValidationListener.java | 482 ++++++ .../bebop/parameters/TimeParameter.java | 153 ++ .../parameters/TrimmedStringParameter.java | 48 + .../TypeCheckValidationListener.java | 74 + .../bebop/parameters/URIParameter.java | 33 + .../parameters/URIValidationListener.java | 84 ++ .../bebop/parameters/URLParameter.java | 63 + .../URLTokenValidationListener.java | 93 ++ .../parameters/URLValidationListener.java | 50 + .../UniqueStringValidationListener.java | 79 + .../parameters/WordValidationListener.java | 52 + .../com/arsdigita/bebop/util/Attributes.java | 130 ++ .../arsdigita/bebop/util/BebopConstants.java | 66 + .../java/com/arsdigita/bebop/util/Color.java | 189 +++ .../bebop/util/GlobalizationUtil.java | 60 + .../bebop/util/PanelConstraints.java | 103 ++ .../arsdigita/bebop/util/SequentialMap.java | 59 + .../java/com/arsdigita/bebop/util/Size.java | 119 ++ .../com/arsdigita/bebop/util/Traversal.java | 128 ++ .../com/arsdigita/bebop/util/package.html | 15 + .../dispatcher/AbortRequestSignal.java | 33 + .../dispatcher/DirectoryListingException.java | 38 + .../dispatcher/DispatcherConstants.java | 93 ++ .../dispatcher/DispatcherHelper.java | 1173 ++++++++++++++ .../dispatcher/InitialRequestContext.java | 250 +++ .../MultipartHttpServletRequest.java | 603 ++++++++ .../dispatcher/RedirectException.java | 42 + .../arsdigita/dispatcher/RequestContext.java | 137 ++ .../globalization/Globalization.java | 542 +++++++ .../globalization/GlobalizationHelper.java | 147 ++ .../arsdigita/globalization/Globalized.java | 53 + .../globalization/GlobalizedMessage.java | 340 +++++ .../ApplicationOIDPatternGenerator.java | 62 + .../ApplicationPatternGenerator.java | 74 + .../templating/HostPatternGenerator.java | 67 + .../templating/LocalePatternGenerator.java | 36 + .../OutputTypePatternGenerator.java | 54 + .../templating/PatternGenerator.java | 28 + .../templating/PatternStylesheetResolver.java | 435 ++++++ .../templating/PrefixPatternGenerator.java | 49 + .../templating/PresentationManager.java | 67 + .../templating/SimplePresentationManager.java | 42 + .../templating/SimpleURIResolver.java | 140 ++ .../templating/StylesheetResolver.java | 40 + .../com/arsdigita/templating/Templating.java | 465 ++++++ .../templating/TemplatingConfig.java | 122 ++ .../templating/URLPatternGenerator.java | 167 ++ .../templating/WebAppPatternGenerator.java | 92 ++ .../WrappedTransformerException.java | 89 ++ .../templating/XSLParameterGenerator.java | 41 + .../com/arsdigita/templating/XSLTemplate.java | 385 +++++ .../java/com/arsdigita/ui/SimplePage.java | 191 +++ .../com/arsdigita/ui/SimplePageLayout.java | 90 ++ .../src/main/java/com/arsdigita/util/IO.java | 57 + .../java/com/arsdigita/util/MessageType.java | 53 + .../java/com/arsdigita/util/OrderedMap.java | 142 ++ .../main/java/com/arsdigita/util/Pair.java | 167 ++ .../com/arsdigita/util/ParameterProvider.java | 39 + .../main/java/com/arsdigita/util/Record.java | 137 ++ .../com/arsdigita/util/SequentialMap.java | 373 +++++ .../com/arsdigita/util/SystemInformation.java | 122 ++ .../java/com/arsdigita/util/URLRewriter.java | 250 +++ .../com/arsdigita/util/servlet/HttpHost.java | 157 ++ .../util/servlet/HttpHostParameter.java | 99 ++ .../web/ApplicationFileResolver.java | 50 + .../arsdigita/web/BaseApplicationServlet.java | 217 +++ .../java/com/arsdigita/web/BaseServlet.java | 159 ++ .../arsdigita/web/CCMDispatcherServlet.java | 407 +++++ .../main/java/com/arsdigita/web/Debugger.java | 105 ++ .../web/DefaultApplicationFileResolver.java | 143 ++ .../arsdigita/web/DynamicHostProvider.java | 26 + .../arsdigita/web/InternalRequestLocal.java | 154 ++ .../com/arsdigita/web/ParameterListener.java | 31 + .../java/com/arsdigita/web/ParameterMap.java | 369 +++++ .../com/arsdigita/web/RedirectSignal.java | 108 ++ .../com/arsdigita/web/TransactionSignal.java | 57 + .../arsdigita/web/TransformationDebugger.java | 175 +++ .../src/main/java/com/arsdigita/web/URL.java | 990 ++++++++++++ .../src/main/java/com/arsdigita/web/Web.java | 777 ++++++++++ .../java/com/arsdigita/web/WebConfig.java | 319 ++++ .../java/com/arsdigita/web/WebContext.java | 151 ++ .../org/libreccm/core/CcmSessionContext.java | 4 + .../src/main/java/org/libreccm/core/User.java | 28 - .../java/org/libreccm/web/Application.java | 6 + .../libreccm/web/ApplicationRepository.java | 52 + .../java/org/libreccm/web/ServletPath.java | 52 + .../java/org/libreccm/docrepo/Resource.java | 6 +- pom.xml | 8 +- 214 files changed, 37588 insertions(+), 32 deletions(-) create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/AbstractSingleSelectionModel.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/BaseLink.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/BasePage.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/Bebop.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/BebopConfig.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/BoxPanel.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/ColumnPanel.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/Completable.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/Component.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/ConfirmPage.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/Container.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/ControlLink.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/DescriptiveComponent.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/Form.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/FormData.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/FormModel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/FormProcessException.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/FormSection.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/FormStep.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/GridPanel.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/Label.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/List.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/Page.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/PageErrorDisplay.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/PageState.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/ParameterSingleSelectionModel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/RequestLocal.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/SessionExpiredException.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/SimpleComponent.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/SimpleContainer.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/SingleSelectionModel.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/ActionEvent.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/ActionListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeEvent.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/EventListenerList.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/FormCancelListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/FormInitListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/FormProcessListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/FormSectionEvent.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/FormSubmissionListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/FormValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/PageEvent.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterEvent.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/PrintEvent.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/PrintListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/RequestEvent.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/RequestListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectModel.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionAdapter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionEvent.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionEvent.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/CheckboxGroup.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/DHTMLEditor.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/Date.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/DateTime.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/Deditor.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/form/Fieldset.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/FileUpload.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/FormErrorDisplay.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/Hidden.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/ImageSubmit.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelect.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelectPairWidget.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/Option.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/OptionGroup.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/Password.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/RadioGroup.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/Reset.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/SearchAndSelect.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/Select.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/SingleSelect.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/Submit.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/TextArea.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/TextEntryFormSection.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/TextField.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/Time.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/form/Widget.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/list/AbstractListModelBuilder.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/list/DefaultListCellRenderer.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/list/ListCellRenderer.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/list/ListModel.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/list/ListModelBuilder.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/page/PageTransformer.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/ArrayParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/BigDecimalParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/BigIntegerParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/BitSetParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/BooleanParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/CancellableValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/DateInRangeValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/DateParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/DateTimeParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/DoubleParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/EmailParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/EmailValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/EnumerationParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/EnumerationValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/FileSizeValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/FloatParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/FloatValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/GlobalizedParameterListener.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/parameters/HTMLColourCodeValidationListener.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/parameters/IncompleteDateParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/IntegerParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/IntegerValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/KeywordValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/LongParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/NotEmptyValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/NotNullValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/NotWhiteSpaceValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/NumberInRangeValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/NumberParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/ParameterData.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/ParameterModel.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/ParameterModelWrapper.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/SingleLineValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/StringInRangeValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/StringIsLettersOrDigitsValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/StringLengthValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/StringParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/TidyHTMLValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/TimeParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/TrimmedStringParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/TypeCheckValidationListener.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/parameters/URIParameter.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/parameters/URIValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLParameter.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLTokenValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLValidationListener.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/parameters/UniqueStringValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/parameters/WordValidationListener.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/util/Attributes.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/util/BebopConstants.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/util/Color.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/util/GlobalizationUtil.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/util/PanelConstraints.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/util/SequentialMap.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/util/Size.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/util/Traversal.java create mode 100755 ccm-core/src/main/java/com/arsdigita/bebop/util/package.html create mode 100644 ccm-core/src/main/java/com/arsdigita/dispatcher/AbortRequestSignal.java create mode 100644 ccm-core/src/main/java/com/arsdigita/dispatcher/DirectoryListingException.java create mode 100644 ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherConstants.java create mode 100644 ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherHelper.java create mode 100644 ccm-core/src/main/java/com/arsdigita/dispatcher/InitialRequestContext.java create mode 100644 ccm-core/src/main/java/com/arsdigita/dispatcher/MultipartHttpServletRequest.java create mode 100644 ccm-core/src/main/java/com/arsdigita/dispatcher/RedirectException.java create mode 100644 ccm-core/src/main/java/com/arsdigita/dispatcher/RequestContext.java create mode 100644 ccm-core/src/main/java/com/arsdigita/globalization/Globalization.java create mode 100644 ccm-core/src/main/java/com/arsdigita/globalization/GlobalizationHelper.java create mode 100644 ccm-core/src/main/java/com/arsdigita/globalization/Globalized.java create mode 100644 ccm-core/src/main/java/com/arsdigita/globalization/GlobalizedMessage.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/ApplicationOIDPatternGenerator.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/ApplicationPatternGenerator.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/HostPatternGenerator.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/LocalePatternGenerator.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/OutputTypePatternGenerator.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/PatternGenerator.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/PatternStylesheetResolver.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/PrefixPatternGenerator.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/PresentationManager.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/SimplePresentationManager.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/SimpleURIResolver.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/StylesheetResolver.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/Templating.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/TemplatingConfig.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/URLPatternGenerator.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/WebAppPatternGenerator.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/WrappedTransformerException.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/XSLParameterGenerator.java create mode 100755 ccm-core/src/main/java/com/arsdigita/templating/XSLTemplate.java create mode 100755 ccm-core/src/main/java/com/arsdigita/ui/SimplePage.java create mode 100755 ccm-core/src/main/java/com/arsdigita/ui/SimplePageLayout.java create mode 100755 ccm-core/src/main/java/com/arsdigita/util/IO.java create mode 100755 ccm-core/src/main/java/com/arsdigita/util/MessageType.java create mode 100644 ccm-core/src/main/java/com/arsdigita/util/OrderedMap.java create mode 100755 ccm-core/src/main/java/com/arsdigita/util/Pair.java create mode 100644 ccm-core/src/main/java/com/arsdigita/util/ParameterProvider.java create mode 100644 ccm-core/src/main/java/com/arsdigita/util/Record.java create mode 100755 ccm-core/src/main/java/com/arsdigita/util/SequentialMap.java create mode 100644 ccm-core/src/main/java/com/arsdigita/util/SystemInformation.java create mode 100644 ccm-core/src/main/java/com/arsdigita/util/URLRewriter.java create mode 100644 ccm-core/src/main/java/com/arsdigita/util/servlet/HttpHost.java create mode 100644 ccm-core/src/main/java/com/arsdigita/util/servlet/HttpHostParameter.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/ApplicationFileResolver.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/BaseApplicationServlet.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/BaseServlet.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/CCMDispatcherServlet.java create mode 100755 ccm-core/src/main/java/com/arsdigita/web/Debugger.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/DefaultApplicationFileResolver.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/DynamicHostProvider.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/InternalRequestLocal.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/ParameterListener.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/ParameterMap.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/RedirectSignal.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/TransactionSignal.java create mode 100755 ccm-core/src/main/java/com/arsdigita/web/TransformationDebugger.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/URL.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/Web.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/WebConfig.java create mode 100644 ccm-core/src/main/java/com/arsdigita/web/WebContext.java create mode 100644 ccm-core/src/main/java/org/libreccm/web/ApplicationRepository.java create mode 100644 ccm-core/src/main/java/org/libreccm/web/ServletPath.java diff --git a/ccm-core/pom.xml b/ccm-core/pom.xml index f82576f16..164765e23 100644 --- a/ccm-core/pom.xml +++ b/ccm-core/pom.xml @@ -88,12 +88,25 @@ commons-codec commons-codec + + commons-fileupload + commons-fileupload + + + commons-lang + commons-lang + oro oro + + net.sf.jtidy + jtidy + + junit junit diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/AbstractSingleSelectionModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/AbstractSingleSelectionModel.java new file mode 100755 index 000000000..16561cd39 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/AbstractSingleSelectionModel.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import java.util.Iterator; + +import com.arsdigita.bebop.event.ChangeEvent; +import com.arsdigita.bebop.event.ChangeListener; +import com.arsdigita.bebop.event.EventListenerList; +import com.arsdigita.util.Assert; +import com.arsdigita.util.Lockable; + +/** + * A standard implementation of SingleSelectionModel and + * Lockable. Those wishing to define a SingleSelectionModel + * will ordinarily want to extend this class. + * + * @version $Id: AbstractSingleSelectionModel.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public abstract class AbstractSingleSelectionModel + implements SingleSelectionModel, Lockable { + + private EventListenerList m_listeners; + private boolean m_locked; + + /** Creates a new AbstractSingleSelectionModel. + */ + public AbstractSingleSelectionModel() { + m_listeners = new EventListenerList(); + } + + /** + * Returns true if there is a selected element. + * + * @param state the state of the current request + * @return true if there is a selected component; + * false otherwise. + */ + public boolean isSelected(PageState state) { + return getSelectedKey(state) != null; + } + + public abstract Object getSelectedKey(PageState state); + + public abstract void setSelectedKey(PageState state, Object key); + + public void clearSelection(PageState state) { + setSelectedKey(state, null); + } + + // Selection change events + + public void addChangeListener(ChangeListener l) { + Assert.isUnlocked(this); + m_listeners.add(ChangeListener.class, l); + } + + public void removeChangeListener(ChangeListener l) { + Assert.isUnlocked(this); + m_listeners.remove(ChangeListener.class, l); + } + + protected void fireStateChanged(PageState state) { + Iterator i = m_listeners.getListenerIterator(ChangeListener.class); + ChangeEvent e = null; + + while (i.hasNext()) { + if ( e == null ) { + e = new ChangeEvent(this, state); + } + ((ChangeListener) i.next()).stateChanged(e); + } + } + + // implement Lockable + public void lock() { + m_locked = true; + } + + public final boolean isLocked() { + return m_locked; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/BaseLink.java b/ccm-core/src/main/java/com/arsdigita/bebop/BaseLink.java new file mode 100755 index 000000000..7f0127a9e --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/BaseLink.java @@ -0,0 +1,545 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.event.PrintEvent; +import com.arsdigita.bebop.event.PrintListener; +import com.arsdigita.globalization.GlobalizedMessage; +import com.arsdigita.util.Assert; +import com.arsdigita.util.UncheckedWrapperException; +import com.arsdigita.xml.Element; +import java.util.TooManyListenersException; + +/** + * The parent of all Bebop Link classes, this class represents a URL on a page. + * It may contain a label, an image, or any other component. + * + *

The following table lists all Bebop Link classes and suggests + * when they might be used. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Link ClassUsage
{@link BaseLink}Parent class of Bebop Link classes. Extend this class to + * build your own Link class.
{@link Link}Link class that manages its own URL variables. Session information + * is added to the target URL for this type.
{@link ExternalLink}Link that does not encode the URL with any session information. + * Used for a link to a page outside the site.
{@link ControlLink} Used for references within its own page (often + * as fields in a table header for sorting a column).
{@link ActionLink}Sets its own control event and runs its own + * {@link com.arsdigita.bebop.event.ActionListener}s. When the link is clicked, + * the code in the Listener's actionPerformed method runs.
{@link ToggleLink}A link that turns into label when it is selected and + * turns back into a link when it is unselected.
+ * + * @version $Id: BaseLink.java 998 2005-11-15 22:27:13Z sskracic $ + */ +public abstract class BaseLink extends DescriptiveComponent + implements Cloneable { + + /** The name of the attribute used in XML to indicate which type of link + * this link represents. */ + private final static String TYPE_ATTR = "type"; + private final static String HREF_NO_JAVASCRIPT = "href_no_javascript"; + private final static String HREF = "href"; + + /** Component used to display the link. Typically a Label, may be + * e.g. an image as well. */ + protected Component m_child; + + /** Property to store the url the Link points to. */ + protected String m_url; + +// Use the parent class' property! +// /** Property to store informational text for the user about the Link, e.g. +// * how to use it, or when to use it (or not to use it). */ +// private GlobalizedMessage m_hint; + + protected String m_noJavascriptURL = null; + + private PrintListener m_printListener; + + private String m_sConfirmMsg = ""; + private GlobalizedMessage m_confirmMsg; + + /** + * Constructor creates a link taking url as the target and display it to + * the user at the same time. It is the only allowed way to present the + * user with a not globlized information. The implementation currently + * miss-uses the Label component to display just a not globalized String + * which is deprecated. + * + * @param url + * @deprecated use BaseLink(Component,url) instead with a Label using a + * GlobalizedMessage instead + */ + public BaseLink(final String url) { + this(new Label(url), url); + } + + /** + * Constructor + * + * @param child display component (Label, Image, etc.) + * @param url URL to point at + */ + public BaseLink(final Component child, final String url) { + super(); + m_url = url; + m_child = child; + } + + /** + * Constructor. + * + * @param child display component (Label, Image, etc.) + * @param listener PrintListener, may be used to change either the Display + * text or the url within a locked page. + */ + public BaseLink(final Component child, final PrintListener listener) { + this(child, ""); + try { + addPrintListener(listener); + } catch (TooManyListenersException e) { + // Can't happen + throw new UncheckedWrapperException("Too many listeners: " + e.getMessage(), e); + } + } + + /** + * Constructor. + * + * @param listener + */ + public BaseLink(final PrintListener listener) { + this("", listener); + } + + // DEPRECATED constructors + + /** + * Constructor. + * + * @param label as text + * @param url + * @deprecated use BaseLink(Component,url) instead with a Label using a + * GlobalizedMessage instead + */ + public BaseLink(final String label, final String url) { + this(new Label(label), url); + } + + /** + * Constructor. + * + * @param label as text + * @param listener PrintListener, may be used to change either the Display + * text or the url within a locked page. + * @deprecated use BaseLink(Component,listener) instead with a Label using + * a GlobalizedMessage instead + */ + public BaseLink(final String label, final PrintListener listener) { + this(new Label(label), listener); + } + + // Class Methods + + /** + * Clone. + * @return + * @throws CloneNotSupportedException + */ + @Override + public Object clone() throws CloneNotSupportedException { + final BaseLink result = (BaseLink) super.clone(); + result.m_printListener = null; + return result; + } + + /** + * Adds a print listener. + * Since the PrintListener is expected to modify the target + * of the PrintEvent, only one print listener can be set + * for a link. + * + * @param listener The print listener. Must not null. + * @throws IllegalArgumentException if listener is null. + * @throws TooManyListenersException if a print listener has previously been + * added. + */ + public void addPrintListener(final PrintListener listener) + throws IllegalStateException, TooManyListenersException { + if (listener == null) { + throw new IllegalArgumentException("Argument listener can not be null"); + } + if (m_printListener != null) { + throw new TooManyListenersException("Too many listeners. Can only have one"); + } + m_printListener = listener; + } + + /** + * Removes a previously added print listener. If the passed in + * listener is not the listener that was added with + * {@link #addPrintListener addPrintListener}, an IllegalArgumentException + * will be thrown. + * + * @param listener The listener that was previously added with + * addPrintListener. + * Must not be null. + * @throws IllegalArgumentException if listener is not the + * currently registered print listener or is null. + */ + public void removePrintListener(final PrintListener listener) + throws IllegalArgumentException { + if (listener == null) { + throw new IllegalArgumentException("listener can not be null"); + } + if (listener != m_printListener) { + throw new IllegalArgumentException("listener is not registered with this widget"); + } + m_printListener = null; + } + + /** + * + * @param state + * @return + */ + protected BaseLink firePrintEvent(final PageState state) { + BaseLink l = this; + if (m_printListener != null) { + try { + l = (BaseLink) this.clone(); + m_printListener.prepare(new PrintEvent(this, state, l)); + } catch (CloneNotSupportedException e) { + l = this; + throw new UncheckedWrapperException(e); + } + } + return l; + } + + /** + * Retrieves the label component used to display the Link. Typically a Label, + * but may be an other type, e.g. an Image, as well. + * + * @return Component used to display the Link. + */ + public final Component getChild() { + return m_child; + } + + public void setChild(final Component child) { + Assert.isUnlocked(this); + m_child = child; + } + + /** + * Use a GlobalizedMessage to be used to display the link. It's primary + * purpose is to hide the parent class' method to prevent its usage because + * Labels and GlobalizedMessages are used here differently (a + * GlobalizedMessage is here not directly used as a Label by specifying it + * as an attribugte, inside a Label component). + * @param message + */ + @Override + public void setLabel(final GlobalizedMessage message) { + Assert.isUnlocked(this); + Label label = new Label(message); + setChild( (Component)label); + + } + + /** + * + * @return + */ + public final String getTarget() { + return m_url; + } + + public final void setTarget(final String url) { + Assert.isUnlocked(this); + + m_url = url; + } + + /** + * Sets the type of link this link represents. + * + * @param type the type of link + */ + protected void setTypeAttr(final String type) { + Assert.isUnlocked(this); + setAttribute(TYPE_ATTR, type); + } + + /** + * + * @param state + * @param parent + */ + protected abstract void generateURL(final PageState state, final Element parent); + + /** + *

Generates a DOM fragment: + *

+     * <bebop:link href="..." type="..." %bebopAttr;/>
+     * 
+ * The href attribute contains the target the link should point + * to. The type attribute is used to give more fine grained + * control over which kind of link this element represents. The types are + * link for a Link, control for a + * {@link ControlLink}, and toggle for a {@link ToggleLink}. + * There may be additional attributes depending on what type of link this + * link represents. + * + * @see ControlLink#generateXML + * @see ToggleLink#generateXML + * + * @param state The current {@link PageState}. + * @param parent The XML element to attach the XML to. + */ + @Override + public void generateXML(final PageState state, final Element parent) { + if (isVisible(state)) { + BaseLink target = firePrintEvent(state); + + Element link = parent.newChildElement("bebop:link", BEBOP_XML_NS); + + target.generateURL(state, link); + target.exportConfirmAttributes(state, link); + //setup the link without javascript + target.setupNoJavascriptURL(state, link); + target.exportAttributes(link); + target.generateExtraXMLAttributes(state, link); + target.generateDescriptionXML(state, link); + target.getChild().generateXML(state, link); + } + } + + /** + * + * @param state + * @param sUrl + * @return + */ + private String getAbsoluteUrl(final PageState state, final String sUrl) { + String sReturn = ""; + + if ((sUrl.indexOf(":") != -1) || sUrl.indexOf("/") == 0) { + //if sUrl contains a ":" or begins with a "/", then it is an absolute URL + sReturn = sUrl; + } else { + //otherwise, it is a relative URL, so we need to make it an absolute URL + + //get the current URL + String sThisURL = ""; + try { + sThisURL = state.stateAsURL(); + } catch (java.io.IOException ioe) { + //ignore + } + //trim the current URL back to the last "/" character + int iIndex = sThisURL.lastIndexOf("/"); + + //if there is no "/" character, then assume we are at server root + if (iIndex == -1) { + sReturn = "/" + sUrl; + } else { + sReturn = sThisURL.substring(0, iIndex) + "/" + sUrl; + } + } + + return sReturn; + } + + /** + * Sets up no-JavaScript fallback HTML + * + * @param state The current {@link PageState}. + * @param link The link element. + */ + protected void setupNoJavascriptURL(final PageState state, final Element link) { + String sURL = null; + + if (m_sConfirmMsg.length() > 0 + || (m_confirmMsg != null && m_confirmMsg.localize().toString().length() > 0)) { + + //if we want the confirm link, create the link + String sOkUrl = getAbsoluteUrl(state, link.getAttribute(HREF)); + String sCancelUrl = null; + try { + sCancelUrl = state.stateAsURL(); + } catch (java.io.IOException e) { + Assert.fail("Could not get current page state as URL"); + } + + if (m_sConfirmMsg.length() > 0) { + sURL = ConfirmPage.getConfirmUrl(m_sConfirmMsg, sOkUrl, sCancelUrl); + } else if (m_confirmMsg != null) { + sURL = ConfirmPage.getConfirmUrl(m_confirmMsg.localize().toString(), sOkUrl, sCancelUrl); + } + + } else { + //don't want confirm link--just no javascript link + if (m_noJavascriptURL == null) { + //get the generatedURL and make it the default noJavaScript link + sURL = link.getAttribute(HREF); + } else { + sURL = m_noJavascriptURL; + } + } + link.addAttribute(HREF_NO_JAVASCRIPT, sURL); + exportAttributes(link); + } + + /** + * Adds type-specific XML attributes to the XML element representing + * this link. Subclasses should override this method if they introduce + * more attributes than the ones {@link #generateXML generateXML} + * produces by default. + * + * @param state The current request + * @param link The XML element representing this link + */ + protected void generateExtraXMLAttributes(final PageState state, final Element link) { + } + + /** + * Sets onClick event and disables the javascript-based double-click + * protection for this link. Not for confirmation messages; Should call + * setConfirmation for that. + * + * @param value The confirmation link. To not use the value {@code return confirm(} with this + * method. + * + * @see #setConfirmation + */ + public void setOnClick(final String value) { + //should not use this method to set confirmation messages--should + //use setConfirmation() instead, or else the javascript will break + if (value != null) { + Assert.isTrue(!value.toLowerCase().startsWith("return confirm("), + "Do not use setOnClick() to set confirmation messages. " + + "Use setCofirmation() instead."); + } + + setAttribute(ON_CLICK, value); + } + + /** + * Forces the user to click through a confirmation dialog before this link + * is followed. The user is prompted with the specified message. If the + * user does not does not confirm, the link is not followed. The current + * implementation uses the JavaScript confirm function and the onClick + * attribute. + * If JavaScript is not enabled in the client browser, this method will + * redirect the browser to a Bebop confirmation page rather than use + * a JavaScript confirmation. + * Subsequent calls to setOnClick will undo the effect of this method. + * + * @param message the confirmation message presented to the user. This + * message cannot have an apostrophe or back slash. + * @deprecated Use setConfirmation(final GlobalizedMessage msg) instead + */ + public void setConfirmation(final String message) { + //make sure that the message doesn't have any apostrophe's + //or back slashes + + if (Assert.isEnabled()) { + final boolean isGoodMessage = message.indexOf("'") == -1 && message.indexOf("\\") == -1; + Assert.isTrue(isGoodMessage, + "confirmation message cannot contain apostrophe or back slash"); + } + + m_sConfirmMsg = message; + } + + /** + * Set a GlobalizedMessage as confirmation message + * @param msg + */ + public void setConfirmation(final GlobalizedMessage msg) { + m_confirmMsg = msg; + } + + /** + * Generate XML output for confirmation links + * + * @param state PageState + * @param link Parent element + */ + private void exportConfirmAttributes(final PageState state, final Element link) { + + // If a confirmation message is set + if (m_sConfirmMsg.length() > 0 || m_confirmMsg != null) { + + // then add the needed attributes to link + link.addAttribute("confirm", "confirm"); + + // If m_sConfirmMsg is not empty + if (m_sConfirmMsg.length() > 0) { + + // then set the onclick attribute for the link with the static message + link.addAttribute(ON_CLICK, "return confirm(\\'" + m_sConfirmMsg + "\\');"); + + // else if m_configMsg is set + } else if (m_confirmMsg != null) { + + //then set the onclick attribute for the link with a globalized message + link.addAttribute(ON_CLICK, "return confirm(\\'" + m_confirmMsg.localize() + "\\');"); + + } + } + } + + public final void setNoJavascriptTarget(final String sURL) { + Assert.isUnlocked(this); + m_noJavascriptURL = sURL; + } + + public final String getNoJavascriptTarget() { + return m_noJavascriptURL; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/BasePage.java b/ccm-core/src/main/java/com/arsdigita/bebop/BasePage.java new file mode 100755 index 000000000..152c044bb --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/BasePage.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.util.Assert; + +/** + * The base page class for use with the PageFactory + * class. It sets two attributes on the XML tag for + * the bebop:page, namely application + * and id. The values for these two + * tags correspond to the parameters passed to the + * PageFactory.buildPage method. + *

+ * This class is intended to be subclassed to provide + * the page infrastructure required by a project, for + * example, adding some navigation components. + * The SimplePage class provides a easy implementation + * whereby the navigation components can be specified + * in the enterprise.init file. + */ +public class BasePage extends Page { + + public BasePage(String application, + Label title, + String id) { + super(title, new SimpleContainer()); + + Assert.exists(application, "application name"); + setAttribute("application", application); + + if (id != null) { + setAttribute("id", id); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Bebop.java b/ccm-core/src/main/java/com/arsdigita/bebop/Bebop.java new file mode 100644 index 000000000..af58f5e2f --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/Bebop.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import org.apache.log4j.Logger; + +/** + * @author Justin Ross + * @see com.arsdigita.bebop.BebopConfig + * @version $Id$ + */ +public final class Bebop { + + private static final Logger s_log = Logger.getLogger(Bebop.class); + + private static BebopConfig s_config = BebopConfig.getInstance(); + + /** + * Gets the BebopConfig object. + */ + public static BebopConfig getConfig() { + return s_config; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/BebopConfig.java b/ccm-core/src/main/java/com/arsdigita/bebop/BebopConfig.java new file mode 100755 index 000000000..f322fa2d4 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/BebopConfig.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.page.PageTransformer; +import com.arsdigita.bebop.util.BebopConstants; +import com.arsdigita.runtime.AbstractConfig; +import com.arsdigita.templating.PresentationManager; +import com.arsdigita.ui.SimplePage; +import com.arsdigita.util.parameter.BooleanParameter; +import com.arsdigita.util.parameter.ClassParameter; +import com.arsdigita.util.parameter.EnumerationParameter; +import com.arsdigita.util.parameter.Parameter; +import com.arsdigita.util.parameter.SingletonParameter; +import com.arsdigita.util.parameter.StringParameter; +import org.apache.log4j.Logger; + +/** + * @author Justin Ross + * @see com.arsdigita.bebop.Bebop + * @version $Id: BebopConfig.java 1498 2007-03-19 16:22:15Z apevec $ + */ +public final class BebopConfig extends AbstractConfig { + + /** A logger instance to assist debugging. */ + private static final Logger s_log = Logger.getLogger(BebopConfig.class); + + /** Singleton config object. */ + private static BebopConfig s_config; + + /** + * Gain a BebopConfig object. + * + * Singleton pattern, don't instantiate a config object using the + * constructor directly! + * @return + */ + public static synchronized BebopConfig getInstance() { + if (s_config == null) { + s_config = new BebopConfig(); + s_config.load(); + } + + return s_config; + } + + // set of configuration parameters + // ///////////////////////////////////////////////////////////////// + + /** + * */ + private final Parameter m_presenter = new SingletonParameter + ("waf.bebop.presentation_manager", Parameter.REQUIRED, + new PageTransformer()); + /** + * + */ + private final Parameter m_page = new ClassParameter + ("waf.bebop.base_page", Parameter.REQUIRED, SimplePage.class); + /** Pointer to JTidy validation listener config file */ + private final Parameter m_tidy = new StringParameter + ("waf.bebop.tidy_config_file", Parameter.REQUIRED, + "com/arsdigita/bebop/parameters/tidy.properties"); + private final Parameter m_fancyErrors = new BooleanParameter + ("waf.bebop.fancy_xsl_errors", + Parameter.REQUIRED, + Boolean.FALSE); + /** Double Click Protection, enabled by default for all buttons in a form.*/ + private final Parameter m_dcpOnButtons = new BooleanParameter + ("waf.bebop.dcp_on_buttons", Parameter.REQUIRED, Boolean.TRUE); + /** Double Click Protection, disabled by default for all links. */ + private final Parameter m_dcpOnLinks = new BooleanParameter + ("waf.bebop.dcp_on_links", Parameter.REQUIRED, Boolean.FALSE); + /** + * + */ + private final Parameter m_enableTreeSelect = new BooleanParameter + ("waf.bebop.enable_tree_select_attribute", + Parameter.REQUIRED, + Boolean.FALSE); + /** List of supported DHTML editors, first one is default (Xinha) */ + private final EnumerationParameter m_dhtmlEditor; + /** Path to DHTML editor source file, relativ to document root */ + private final Parameter m_dhtmlEditorSrcFile; + /** */ + private final Parameter m_showClassName = new BooleanParameter + ("waf.bebop.show_class_name", Parameter.OPTIONAL, Boolean.FALSE); + + /** + * Constructor. + * Singelton pattern, don't instantiate a config object using the + * constructor directly! Use getConfig() instead. + * + */ + public BebopConfig() { + + /** List of supported DHTML editors, first one is default (Xinha) */ + m_dhtmlEditor = new EnumerationParameter("waf.bebop.dhtml_editor", + Parameter.REQUIRED,BebopConstants.BEBOP_XINHAEDITOR); + m_dhtmlEditor.put("Xinha", BebopConstants.BEBOP_XINHAEDITOR); + m_dhtmlEditor.put("FCKeditor", BebopConstants.BEBOP_FCKEDITOR); + // HTMLArea for backwards compatibility with old XSL. to be removed soon! + m_dhtmlEditor.put("HTMLArea", BebopConstants.BEBOP_DHTMLEDITOR); + + // Xinha is now default! + m_dhtmlEditorSrcFile = new StringParameter + ("waf.bebop.dhtml_editor_src", Parameter.REQUIRED, + "/assets/xinha/XinhaLoader.js"); + + register(m_presenter); + register(m_page); + register(m_tidy); + register(m_fancyErrors); + register(m_dhtmlEditor); + register(m_dhtmlEditorSrcFile); + register(m_dcpOnButtons); + register(m_dcpOnLinks); + register(m_enableTreeSelect); + register(m_showClassName); + + loadInfo(); + } + + /** + * Gets the configured PresentationManger. + */ + public final PresentationManager getPresentationManager() { + return (PresentationManager) get(m_presenter); + } + + final Class getBasePageClass() { + return (Class) get(m_page); + } + + /** + * I don't *want* to make this public. XXX + */ + public final String getTidyConfigFile() { + return (String) get(m_tidy); + } + + public boolean wantFancyXSLErrors() { + return ((Boolean)get(m_fancyErrors)).booleanValue(); + } + + public final boolean doubleClickProtectionOnButtons() { + return ((Boolean) get(m_dcpOnButtons)).booleanValue(); + } + + public final boolean doubleClickProtectionOnLinks() { + return ((Boolean) get(m_dcpOnLinks)).booleanValue(); + } + + public final boolean treeSelectAttributeEnabled() { + return ((Boolean) get(m_enableTreeSelect)).booleanValue(); + } + + /** + * Gets the DHTML editor to use + */ + public final String getDHTMLEditor() { + return (String) get(m_dhtmlEditor); + } + + /** + * Gets the location of DHTML editor's source file + */ + public final String getDHTMLEditorSrcFile() { + return (String) get(m_dhtmlEditorSrcFile); + } + + public final boolean showClassName() { + return ((Boolean) get(m_showClassName)).booleanValue(); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/BoxPanel.java b/ccm-core/src/main/java/com/arsdigita/bebop/BoxPanel.java new file mode 100755 index 000000000..f58d6046c --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/BoxPanel.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import java.util.Iterator; + + +import com.arsdigita.xml.Element; + +import com.arsdigita.bebop.form.Hidden; + +// This interface contains the XML element name of this class +// in a constant which is used when generating XML +import com.arsdigita.bebop.util.BebopConstants; +import com.arsdigita.bebop.util.PanelConstraints; + +/** + * A container that prints its components in one row, either horizontally or + * vertically. + * + * @author David Lutterkort + * @version $Id: BoxPanel.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class BoxPanel extends SimpleContainer + implements BebopConstants, PanelConstraints { + + /** Specifies that components should be laid out left to right. */ + public final static int HORIZONTAL = 1; + + /** Specifies that components should be laid out top to bottom. */ + public final static int VERTICAL = 2; + + /** XML attribute for width */ + private static final String WIDTH_ATTR = "width"; + + /** XML attribute wether to draw a border. */ + private static final String BORDER_ATTR = "border"; + + /** Property to whether to draw a HORIZONTAL or VERTICAL box panel. */ + private int m_axis; + + /** Property to store whether to to center alignment. */ + private boolean m_centering; + + + /** + * Constructor, creates a box panel that lays out its components from + * top to bottom. The components are not centered. + */ + public BoxPanel() { + this(VERTICAL); + } + + /** + * Constructor, creates a box panel that lays out its components in the given + * direction. The components are not centered. + * + * @param axis the axis to use to lay out the components + */ + public BoxPanel(int axis) { + this(axis, false); + } + + /** + * Creates a box panel that lays out its components in the given + * direction and centers them if that is specified. + * + * @param axis the axis to use to lay out the components + * @param centering true if the layout should be centered + */ + public BoxPanel(int axis, boolean centering) { + m_axis = axis; + m_centering = centering; + } + + // Instance methods + + /** + * Sets the width attribute of the box panel. The given width should be in + * a form that is legal as the width attribute of an HTML + * table element. + * + * @param w the width of the box panel + */ + public void setWidth(String w) { + setAttribute(WIDTH_ATTR, w); + } + +// /** +// * Sets whether a border should be drawn. +// * +// * @param isBorder true if a border should be drawn +// * @deprecated Use {@link #setBorder(int border)} instead. +// */ +// public void setBorder(boolean isBorder) { +// if (isBorder) { +// setAttribute(BORDER_ATTR, "1"); +// } else { +// setAttribute(BORDER_ATTR, "0"); +// } +// } + + /** + * + * Sets the width of the border to draw around the components. This value + * will be used for the border attribute in an HTML + * table element. + * + * @param border the width of the border + */ + public void setBorder(int border) { + setAttribute(BORDER_ATTR, String.valueOf(border)); + } + + /** + * Adds nodes for the panel and its child components to be rendered, + * usually in a table. Any hidden widgets directly contained in the box + * panel are added directly to parent and are not in any + * of the cells that the box panel generates. + * + *

Generates DOM fragment: + *

<bebop:boxPanel [width=...] border=... center... axis...> + * <bebop:cell> cell contents </bebop:cell> + * </bebop:boxPanel> + * + * @param parent + */ + @Override + public void generateXML(PageState state, Element parent) { + if (isVisible(state)) { + Element panel = parent.newChildElement(BEBOP_BOXPANEL, BEBOP_XML_NS); + // or: rowPanel/columPanel? + panel.addAttribute("center", String.valueOf(m_centering)); + panel.addAttribute("axis", String.valueOf(m_axis)); + exportAttributes(panel); + + for (Iterator i = children(); i.hasNext();) { + Component c = (Component) i.next(); + + if (c.isVisible(state)) { + if (c instanceof Hidden) { + c.generateXML(state, parent); + } else { + Element cell = panel.newChildElement(BEBOP_CELL, BEBOP_XML_NS); + c.generateXML(state, cell); + } + } + } + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/ColumnPanel.java b/ccm-core/src/main/java/com/arsdigita/bebop/ColumnPanel.java new file mode 100755 index 000000000..ee68b7769 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/ColumnPanel.java @@ -0,0 +1,574 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.form.Hidden; +import com.arsdigita.bebop.util.Attributes; +import com.arsdigita.bebop.util.PanelConstraints; +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Element; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * A container that prints its components in a table. Each child is printed + * in its own table cell. The number of columns can be specified in the + * constructor. The components are put into the table in the order in which + * they were added to the ColumnPanel by filling the table one row + * at a time (filling each row from left to right), from the top of the table + * to the bottom. + * + *

The position of the component within the cell can be influenced with the + * following constraints. + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Horizontal alignmentUse LEFT, CENTER, or + * RIGHT.
Vertical alignmentUse TOP, MIDDLE, or + * BOTTOM.
Full widthUse FULL_WIDTH to instruct the panel to + * put the component in a row by itself, spanning the full width of the + * table.
Inserting childrenUse INSERT to instruct the panel to + * insert the corresponding component, assuming that it will also be + * laid out by a ColumnPanel with the same number of + * columns.
+ * + *

Constraints can be combined by OR-ing them together. For example, to print + * a component in a row of its own, left-aligned, at the bottom of its cell, + * use the constraint FULL_WIDTH | LEFT | BOTTOM. + * + *

Using the INSERT constraint fuses the current ColumnPanel + * with the panel of the child to which the constraint is applied. For example, + * consider a {@link Form} that is to have a 2-column format with labels in the + * left column and widgets in the right column. If a {@link FormSection} is + * added to the form, it should be included seamlessly into the parent form. + * To do this, set the INSERT constraint when the {@link + * FormSection} is added to the ColumnPanel of the {@link Form}. At + * the same time, tell the ColumnPanel used to lay out the {@link + * FormSection} that it is to be inserted into another panel. + * + *

The following pseudo-code illustrates the example. (It assumes that + * Form and FormSection are decorators of the ColumnPanel.) + * + *

+ *
+ *   Form form = new Form(new ColumnPanel(2));
+ *   FormSection sec = new FormSection(new ColumnPanel(2, true));
+ *
+ *   sec.add(new Label("Basic Item Metadata"), ColumnPanel.FULL_WIDTH);
+ *   sec.add(new Label("Title:"), ColumnPanel.RIGHT);
+ *   sec.add(new Text("title"));
+ *
+ *   form.add(sec, ColumnPanel.INSERT);
+ * 
+ * + * @author David Lutterkort + * @version $Id: ColumnPanel.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class ColumnPanel extends SimpleContainer + implements PanelConstraints { + + /** An empty constraint corresponding to the default */ + private static final Constraint DEFAULT_CONSTRAINT = new Constraint(); + + /** The number of columns in the panel */ + private int m_nCols; + + /** Explicitly registered constraints for child components. Maps + * Componentss to Constraints */ + private Map m_constraints; + + /** Is this panel inserted in another one ? If so, do not produce + * <table> tags */ + private boolean m_inserted; + + /** Border attributes */ + private Attributes m_border; + private Attributes m_padFrame; + private Attributes m_pad; + private String[] m_columnWidth; + + +// Instance methods + + /** + * Creates a table panel with the specified number of columns. + * + * @param nCols number of columns in the panel + */ + public ColumnPanel(int nCols) { + this(nCols, false); + makeBorder(); + makePadFrame(); + makePad(); + } + + /** + * Creates a table panel with the specified number of columns that will + * be printed as a direct child of a ColumnPanel + * with the same number of columns. + * @see #setInserted + * + * @param nCols number of columns in the panel + * @param inserted true if this panel + * is to be printed as a direct child of a + * ColumnPanel with the same number of + * columns + */ + public ColumnPanel(int nCols, boolean inserted) { + m_nCols = nCols; + setInserted(inserted); + m_constraints = new HashMap(); + m_columnWidth=new String[nCols]; + } + + /** + * Adds a component, specifying its constraints. + * + * @param c the component to add + * @param constraints the constraints for this component + */ + @Override + public void add(Component c, int constraints) { + super.add(c); + setConstraint(c, constraints); + } + + /** + * Sets whether this panel will be printed inside a + * ColumnPanel with the same number of columns. If + * inserted is true, no <table> tags will be produced + * to enclose the child components. + * @param inserted true if this panel is to be printed + * inside a ColumnPanel with the same number of columns + */ + public void setInserted(boolean inserted) { + Assert.isUnlocked(this); + m_inserted = inserted; + } + + /** + * Determines whether this panel is to be inserted into another panel. + * @return true if this panel is to be inserted + * into another panel; false otherwise. + * + * @see #setInserted + */ + public final boolean isInserted() { + return m_inserted; + } + + /** + * Returns the number of columns for this ColumnPanel + * @return the number of columns + * + */ + public final int getNumCols() { + return m_nCols; + } + + /** + * Adds child components as a subtree under table-style nodes. If any of the + * direct children are hidden form widgets, they are added directly to + * parent rather than included in any of the + * cell elements of the panel. + * + *

Generates a DOM fragment: + *

+     * <bebop:pad>
+     *   [<bebop:padFrame>]
+     *    [<bebop:border>]
+     *      <bebop:panelRow>
+     *       <bebop:cell> ... cell contents </bebop:cell>
+     *       <bebop:cell> ... cell contents </bebop:cell>
+     *       ...
+     *      </bebop:panelRow>
+     *      <bebop:panelRow>
+     *       <bebop:cell> ... cell contents </bebop:cell>
+     *       <bebop:cell> ... cell contents </bebop:cell>
+     *       ...
+     *      </bebop:panelRow>
+     *    [</bebop:border>]
+     *   [</bebop:padFrame>]
+     * </bebop:boxPanel>
+ * @param state the current page state + * @param parent the parent element for these child components + */ + @Override + public void generateXML(PageState state, Element parent) { + if ( isVisible(state) ) { + + Element panel = parent.newChildElement("bebop:columnPanel", BEBOP_XML_NS); + exportAttributes(panel); + // parent.addContent(panel); + + generateChildren(state, parent, generateTopNodes(panel)); + } + } + + // Border attributes + + private void makeBorder() { + if ( m_border == null ) { + m_border = new Attributes(); + m_border.setAttribute("cellspacing", "0"); + m_border.setAttribute("cellpadding", "4"); + m_border.setAttribute("border", "0"); + m_border.setAttribute("width", "100%"); + } + } + + /** + * + * + * @param c + */ + public void setBorderColor(String c) { + makeBorder(); + m_border.setAttribute("bgcolor", c); + } + + /** + * + * + * @param w + */ + public void setBorderWidth(String w) { + makeBorder(); + m_border.setAttribute("cellpadding", w); + } + + public void setColumnWidth(int col, String width) { + m_columnWidth[col-1]=width; + } + + /** + * + * + * @param b + */ + public void setBorder(boolean b) { + if (b) { + makeBorder(); + } else { + m_border = null; + } + } + + // Pad and Padframe attributes + private void makePadFrame() { + if (m_padFrame == null) { + m_padFrame = new Attributes(); + m_padFrame.setAttribute("cellspacing", "0"); + m_padFrame.setAttribute("cellpadding", "6"); + m_padFrame.setAttribute("border", "0"); + m_padFrame.setAttribute("width", "100%"); + } + } + + /** + * + * + */ + private void makePad() { + if ( m_pad == null ) { + m_pad = new Attributes(); + m_pad.setAttribute("cellspacing", "0"); + m_pad.setAttribute("cellpadding", "2"); + m_pad.setAttribute("border", "0"); + m_pad.setAttribute("width", "100%"); + } + } + + /** + * + * + * @param c + */ + public void setPadColor(String c) { + makePadFrame(); + makePad(); + m_padFrame.setAttribute("bgcolor", c); + m_pad.setAttribute("bgcolor", c); + } + + /** + * + * + * @param w + */ + public void setWidth(String w) { + makePadFrame(); + m_padFrame.setAttribute("width", w); + } + + /** + * + * + * @param w + */ + public void setPadFrameWidth(String w) { + makePadFrame(); + m_padFrame.setAttribute("cellpadding", w); + } + + /** + * + * + * @param border + */ + public void setPadBorder(boolean border) { + makePad(); + if(border) { + m_pad.setAttribute("border", "1"); + } else { + m_pad.setAttribute("border", "0"); + } + } + + /** + * + * + * @param padding + */ + public void setPadCellPadding(String padding) { + makePad(); + m_pad.setAttribute("cellpadding", padding); + } + + /** + * add top tags (will translate to opening/closing), + * including display styles + */ + private Element generateTopNodes(Element parent) { + // FIXME: set background color, border effects, cell spacing etc. + if (isInserted()) { + return parent; + } + String l_class = getClassAttr(); + if (m_border != null) { + Element border = parent.newChildElement("bebop:border",BEBOP_XML_NS); + if (l_class != null) { + m_border.setAttribute("class", l_class); + } + m_border.exportAttributes(border); + // parent.addContent(border); + parent=border; // nest the rest inside border + } + if ( m_padFrame != null ) { + Element padFrame = parent.newChildElement("bebop:padFrame", BEBOP_XML_NS); + if (l_class != null) { + m_padFrame.setAttribute("class", l_class); + } + m_padFrame.exportAttributes(padFrame); + // parent.addContent(padFrame); + parent=padFrame; // nest the rest in padFrame + } + Element pad = parent.newChildElement("bebop:pad", BEBOP_XML_NS); + if (l_class != null) { + m_pad.setAttribute("class", l_class); + } + m_pad.exportAttributes(pad); + // parent.addContent(pad); + return pad; + } + + /** + * Lay out the child components using constraints registered for them, + * generating a DOM tree and extending another. + * + * @param state represents the state of the current request + * @param hiddenParent the element to which hiddens are added + * @param parent the element to which ordinary rows and cells are added + */ + private void generateChildren(PageState state, Element hiddenParent, + Element parent) { + // Count the number of components printed in the current row + int rowLen = m_nCols + 1; // Force generation of first row + Element row = null; + Element cell = null; + + for (Iterator i = children(); i.hasNext(); ) { + Component c = (Component) i.next(); + + if ( c.isVisible(state) ) { + + if ( c instanceof Hidden ) { + c.generateXML(state, hiddenParent); + } else { + if ( isInsert(c) ) { + c.generateXML(state, parent); + rowLen = m_nCols + 1; // Force generation of new row + } else { + if ( rowLen >= m_nCols || isFullWidth(c)) { + rowLen = 0; + row = parent.newChildElement("bebop:panelRow", BEBOP_XML_NS); + // parent.addContent(row); + } + cell = row.newChildElement("bebop:cell", BEBOP_XML_NS); + // row.addContent(cell); + if ( m_columnWidth[rowLen] != null ) { + cell.addAttribute("width", m_columnWidth[rowLen]); + } + getConstraint(c).exportAttributes(cell, m_nCols); + c.generateXML(state, cell); + rowLen++; + if ( isFullWidth(c) ) { + // Force a new row if c was full width + rowLen = m_nCols + 1; + } + } + } + } + } + } + + /** + * Sets the constraint for one child component. + * @param c the child component + * @param constraints the constraints to add + */ + public void setConstraint(Component c, int constraints) { + Assert.isUnlocked(this); + m_constraints.put(c, new Constraint(constraints)); + } + + /** + * Get the constraint object for a component. If no constraints have been + * set explicitly, return a default constraint object. + * + * @post return != null + */ + private Constraint getConstraint(Component c) { + Constraint result = (Constraint) m_constraints.get(c); + if ( result == null ) { + return DEFAULT_CONSTRAINT; + } else { + return result; + } + } + + private boolean isInsert(Component c) { + return getConstraint(c).isInsert(); + } + + private boolean isFullWidth(Component c) { + return getConstraint(c).isFullWidth(); + } + + + // Inner class(es) + + /** + * Represent the constraints for one child component + */ + private static class Constraint { + private boolean m_fullWidth; + private boolean m_insert; + private String m_alignment; // for print + private String m_halign; // for generateXML + private String m_valign; // for generateXML + + public Constraint() { + this(0); + } + + public Constraint(int constraints) { + StringBuilder s = new StringBuilder(); + + if ( (constraints & (LEFT|CENTER|RIGHT)) != 0 ) { + s.append(" align=\""); + if ( (constraints & LEFT) != 0) { + s.append(m_halign = "left"); + } else if ( (constraints & CENTER) != 0) { + s.append(m_halign = "center"); + } else if ( (constraints & RIGHT) != 0) { + s.append(m_halign = "right"); + } + s.append("\" "); + } else { + m_halign = null; + } + + if ( (constraints & (TOP|MIDDLE|BOTTOM)) != 0 ) { + s.append(" valign=\""); + if ( (constraints & TOP) != 0) { + s.append(m_valign = "top"); + } else if ( (constraints & MIDDLE) != 0) { + s.append(m_valign = "middle"); + } else if ( (constraints & BOTTOM) != 0) { + s.append(m_valign = "bottom"); + } + s.append("\" "); + } else { + m_valign = null; + } + + m_alignment = s.toString(); + + m_fullWidth = (constraints & FULL_WIDTH) != 0; + m_insert = (constraints & INSERT) != 0; + } + + public final boolean isFullWidth() { + return m_fullWidth; + } + + public final boolean isInsert() { + return m_insert; + } + + public final String getAlignment() { + return m_alignment; + } + + public final String getHAlign() { + return m_halign; + } + + public final String getVAlign() { + return m_valign; + } + + public void exportAttributes(Element cell, int nCols) { + String halign = getHAlign(); + String valign = getVAlign(); + if (halign != null) { + cell.addAttribute("align" , halign); + } + if (valign != null) { + cell.addAttribute("valign", valign); + } + if ( isFullWidth() ) { + cell.addAttribute("colspan", Integer.toString(nCols)); + } + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Completable.java b/ccm-core/src/main/java/com/arsdigita/bebop/Completable.java new file mode 100755 index 000000000..5afc2dae3 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/Completable.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.util.Assert; + +import com.arsdigita.bebop.event.ActionEvent; +import com.arsdigita.bebop.event.ActionListener; +// Stacktraces is a support tool to use in a specifically difficult development +// situation. It is abundant in production and for normal development work and +// it provved to have funny side effects in a production environment. So it is +// commented out here but kept for further references. +// import com.arsdigita.developersupport.StackTraces; + +import java.util.ArrayList; +import java.util.Iterator; + +import org.apache.log4j.Logger; + +/** + * Completable. + * + * @author rhs@mit.edu + * @version $Revision: #10 $ $Date: 2004/08/16 $ + * @version $Id: Completable.java 287 2005-02-22 00:29:02Z sskracic $ + **/ + +public abstract class Completable implements Component { + + private final static Logger s_log = Logger.getLogger(Completable.class); + + private ArrayList m_completionListeners = new ArrayList(); + + public Completable() { + // See note above! + // if ( s_log.isDebugEnabled() ) { + // StackTraces.captureStackTrace(this); + // } + } + + public void addCompletionListener(ActionListener listener) { + Assert.isUnlocked(this); + m_completionListeners.add(listener); + } + + protected void fireCompletionEvent(PageState ps) { + ActionEvent evt = new ActionEvent(this, ps); + for (Iterator it = m_completionListeners.iterator(); it.hasNext(); ) { + ActionListener listener = (ActionListener) it.next(); + listener.actionPerformed(evt); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Component.java b/ccm-core/src/main/java/com/arsdigita/bebop/Component.java new file mode 100644 index 000000000..b0b83b748 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/Component.java @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import java.util.Iterator; + +import com.arsdigita.util.Lockable; +import com.arsdigita.xml.Element; + +/** + * The common interface implemented by all Bebop components. + * + * During its lifetime, a component receives the following calls + * from the containing page. + * + * + * + * + *

Visibility

+ * + *

A component can be either visible or + * invisible. Invisible components do not produce any output and + * containers should be careful to completely hide their presence. The + * visibility of a component can be influenced in a number of ways: + *

+ *

The {@link Page} makes sure that the visibility of components is + * preserved across repeated requests to the same page. + * + *

Standard Attributes

+ *

Each component supports a few standard attributes that are copied + * through to the output when producing either HTML or XML + * output. These attributes are not used internally in any way, and setting + * them is entirely optional. + * + *

The standard attributes appear in the output as attributes in + * the element generated from this component. They correspond directly to + * properties with setters and getters. The standard attributes are as folows. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Attribute Java property Purpose
id + * {@link #getIdAttr getIdAttr}/{@link #setIdAttr setIdAttr} + * Use to uniquely identify a component within the page. Uniqueness is + * not enforced. The id attribute allows stylesheet designers to + * access individual components.
class{@link #getClassAttr()}/{@link + * #setClassAttr(String)}Use as the generic name for a + * component. For example, if you have a UserInfoDisplay + * component, the class attribute can be used to name the + * component "UserInfoDisplay" so that a generic template rule + * can style all UserInfoDisplay components.
style{@link #getStyleAttr getStyleAttr}/{@link + * #setStyleAttr setStyleAttr}Use to add CSS style to an individual element.
+ * + *

Caveat: Race Conditions

+ *

+ * When extending any Component, honor the + * Lockable contract indicated by extends {@link + * com.arsdigita.util.Lockable}. Beware that member variables + * are not inherently threadsafe, because you may be circumventing + * the contract. For variables that might be different for each + * request, use {@link RequestLocal}. If you must add member + * variables in the derived class, as a minimum be sure to safeguard + * any write access to instance variables with {@link + * com.arsdigita.util.Assert#assertNotLocked}. + *

+ * @author David Lutterkort + * @author Stanislav Freidin + * @author Rory Solomon + * + * @version $Id$ + */ +public interface Component extends Lockable { + + /** + * The XML namespace used by all the Bebop components. + */ + String BEBOP_XML_NS = + "http://www.arsdigita.com/bebop/1.0"; + + /** + * The name for the class attribute. + * @see #setClassAttr(String) + * @see Standard Attributes + */ + String CLASS = "class"; + + /** + * The name for the style attribute. + * @see #setStyleAttr(String) + * @see Standard Attributes + */ + String STYLE = "style"; + + /** + * The name for the ID attribute. + * @see #setIdAttr + * @see Standard Attributes + */ + String ID = "id"; + + // HTML 4 event names + + /** + * The onClick event. + */ + String ON_CLICK = "onclick"; + + /** + *

Adds a DOM subtree representing this component under the given + * parent node. Uses the request values stored in state.

+ * + * @param state represents the current request + * @param parent the node under which the DOM subtree should be added + * + * @pre state != null + * @pre parent != null + */ + void generateXML(PageState state, Element parent); + + + /** + *

Responds to the request. This method is only called if the request + * was made from a link or form that the component put on the page in the + * {@link PageState#stateAsURL} previous request.

+ * + *

No output should be generated on the HTTP response. The component + * can store intermediate results in the state by calling + * {@link PageState#setAttribute setAttribute}.

+ * + *

This method is called before any output is printed to the HTTP + * response so that the component can forward to a different page and + * thereby commit the response.

+ * + * @param state represents the current request + * + * @pre state != null + */ + void respond(PageState state) + throws javax.servlet.ServletException; + + /** + * Returns an iterator over the children of this component. If the + * component has no children, returns an empty (not + * null) iterator. + * + * @return an iterator over the children of this component. + * + * @post return != null + */ + Iterator children(); + + /** + * Registers state parameters for the page with its model. + * + * A simple component with a state parameter param would do + * the following in the body of this method: + *
+     *   p.addComponent(this);
+     *   p.addComponentStateParam(this, param);
+     * 
+ * + * You should override this method to set the default visibility + * of your component: + * + *
+     * public void register(Page p) {
+     *     super.register(p);
+     *     p.setVisibleDefault(childNotInitiallyShown,false);
+     *     p.setVisibleDefault(anotherChild, false);
+     * }
+     * 
+ * + * Always call super.register when you override + * register. Otherwise your component may + * malfunction and produce errors like "Widget ... isn't + * associated with any Form" + * + * @param p + * @pre p != null + */ + void register(Page p); + + /** + * Registers form parameters with the form model for this + * form. This method is only important for {@link FormSection form + * sections} and {@link com.arsdigita.bebop.form.Widget widgets} + * (components that have a connection to an HTML form). Other + * components can implement it as a no-op. + * + * @param f + * @param m + * @pre f != null + * @pre m != null + */ + void register(Form f, FormModel m); + + /* Properties that will get copied straight to the output, + both in HTML and in XML + */ + + /** + * Gets the class attribute. + * + * @return the class attribute. + * + * @see #setClassAttr(String) + * @see Standard Attributes + */ + String getClassAttr(); + + /** + * Sets the class attribute. + * @param theClass a valid XML name + * @see Standard Attributes + * @see #getClassAttr + */ + void setClassAttr(String theClass); + + /** + * Gets the style attribute. + * + * @return the style attribute. + * + * @see #setStyleAttr + * @see Standard Attributes + */ + String getStyleAttr(); + + /** + * Sets the style attribute. style should be a valid CSS + * style, because its value will be copied verbatim to the output and + * appear as a style attribute in the top level XML or HTML + * output element. + * + * @param style a valid CSS style description for use in the + * style attribute of an HTML tag + * @see Standard Attributes + */ + void setStyleAttr(String style); + + /** + * Gets the id attribute. + * + * @return the id attribute. + * + * @see Standard Attributes + * @see #setIdAttr(String id) + */ + String getIdAttr(); + + /** + * Sets the id attribute. id + * should be an XML name + * that is unique within the {@link Page Page} in which this component is + * contained. The value of id is copied literally to the + * output and not used for internal processing. + * + * @param id a valid XML identifier + * @see Standard Attributes + */ + void setIdAttr(String id); + + /** + * Supplies a key for making parameter names unique. To be used + * instead of the component's index (see Component Prefix). + * To avoid collision with indexOf, it + * should (1) be a legal fragment of a cgi parameter, (2) differ from "g", + * and (3) not start with a digit. + * + * @param key + * @return + */ + Component setKey(String key); + + /** + * Retrieves the programmer-supplied key. Normally, there is no + * such key and the method returns null. + * + * @return the programmer-supplied key. + */ + String getKey(); + + /** + * Determines whether the component is visible in the request + * represented by state. + * @see #setVisible setVisible + * @see Description of Visibility + * above + * + * + * @param state represents the current request + * @return true if the component is visible; false + * otherwise. + * @pre state != null + */ + boolean isVisible(PageState state); + + /** + * Changes the visibility of the component. The component will keep the + * visibility that is set with this method in subsequent requests to this page. + * + * @param state represents the current request + * @param v true if the component should be visible + * @pre state != null + * @see Description of Visibility + * above + */ + void setVisible(PageState state, boolean v); + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/ConfirmPage.java b/ccm-core/src/main/java/com/arsdigita/bebop/ConfirmPage.java new file mode 100755 index 000000000..628ed4892 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/ConfirmPage.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + + +import com.arsdigita.bebop.event.FormInitListener; +import com.arsdigita.bebop.event.FormProcessListener; +import com.arsdigita.bebop.event.FormSectionEvent; +import com.arsdigita.bebop.event.PrintEvent; +import com.arsdigita.bebop.event.PrintListener; +import com.arsdigita.bebop.form.Submit; +import com.arsdigita.bebop.parameters.StringParameter; +import com.arsdigita.web.ParameterMap; +import com.arsdigita.web.RedirectSignal; +import com.arsdigita.web.URL; + +/** + * A Bebop Confirmation Page which should be mounted at ConfirmPage.CONFIRM_URL by the BebopMapDispatcher. + * This page takes three URL parameters: + * + * The page displays a form asking the confirmation message passed in. If the user hits OK, + * Then the page redirects to the OK URL. Otherwise, if the user hits Cancel, + * The page redirects to the Cancel URL. + * @author Bryan Che + */ + +public class ConfirmPage extends Page { + private StringParameter m_ConfirmMsgParam; + private StringParameter m_OkUrlParam; + private StringParameter m_CancelUrlParam; + + private RequestLocal m_ConfirmMsgRL; + private RequestLocal m_OkUrlRL; + private RequestLocal m_CancelUrlRL; + + //URL at which to mount this page + public static final String CONFIRM_URL = "BEBOP-confirmation-page"; + + //URL variable names + private static final String CONFIRM_MSG_VAR = "confirm-msg"; + private static final String OK_URL_VAR = "ok-url"; + private static final String CANCEL_URL_VAR = "cancel-url"; + + public ConfirmPage() { + super(); + + m_ConfirmMsgParam = new StringParameter(CONFIRM_MSG_VAR); + m_OkUrlParam = new StringParameter(OK_URL_VAR); + m_CancelUrlParam = new StringParameter(CANCEL_URL_VAR); + + //add global state params + addGlobalStateParam(m_ConfirmMsgParam); + addGlobalStateParam(m_OkUrlParam); + addGlobalStateParam(m_CancelUrlParam); + + //initialize RequestLocals for the URL params + m_ConfirmMsgRL = new RequestLocal() { + protected Object initialValue(PageState ps) { + return ps.getValue(m_ConfirmMsgParam); + } + }; + m_OkUrlRL = new RequestLocal() { + protected Object initialValue(PageState ps) { + return ps.getValue(m_OkUrlParam); + } + }; + m_CancelUrlRL = new RequestLocal() { + protected Object initialValue(PageState ps) { + return ps.getValue(m_CancelUrlParam); + } + }; + + //set the title + buildTitle(); + + //add the form + ConfirmForm cf = new ConfirmForm(m_ConfirmMsgRL, m_OkUrlRL, m_CancelUrlRL); + add(cf); + + lock(); + } + + /** + * Returns a URL (minus "http://" string and server name) at which to access the ConfirmPage + * with the given Confirmation Message, OK URL, and Cancel URL. + * @param sConfirmMsg the Confirmation message to display on the page + * @param sOkUrl the URL to which to redirect if the user hits OK + * @param sCancelUrl the URL to which to redirect if the user hits Cancel + * @return URL at which to access the ConfirmPage + */ + public static String getConfirmUrl(String sConfirmMsg, String sOkUrl, String sCancelUrl) { + final ParameterMap params = new ParameterMap(); + + params.setParameter(CONFIRM_MSG_VAR, sConfirmMsg); + params.setParameter(OK_URL_VAR, sOkUrl); + params.setParameter(CANCEL_URL_VAR, sCancelUrl); + + return URL.there("/" + CONFIRM_URL, params).toString(); + } + + protected void buildTitle() { + class ConfirmPagePrintListener implements PrintListener { + public void prepare(PrintEvent e) { + Label label = (Label) e.getTarget(); + PageState ps = e.getPageState(); + + label.setLabel((String)m_ConfirmMsgRL.get(ps)); + } + } + + setTitle(new Label(new ConfirmPagePrintListener())); + } + + private class ConfirmFormPrintListener implements PrintListener { + private RequestLocal m_RL; + + ConfirmFormPrintListener(RequestLocal ConfirmMsgRL) { + m_RL = ConfirmMsgRL; + } + + public void prepare(PrintEvent e) { + Label label = (Label) e.getTarget(); + PageState ps = e.getPageState(); + + label.setLabel((String)m_RL.get(ps) ); + } + } + + private class ConfirmForm extends Form implements FormInitListener, FormProcessListener { + private Label m_ConfirmMsgLabel; + private Submit m_OkButton; + private Submit m_CancelButton; + + private RequestLocal m_OkRL; + private RequestLocal m_CancelRL; + + private String m_sOkUrl = null; + private String m_sCancelUrl = null; + + public ConfirmForm(RequestLocal ConfirmMsgRL, RequestLocal OkUrlRL, RequestLocal CancelUrlRL) { + super("ConfirmForm"); + m_ConfirmMsgLabel = new Label(new ConfirmFormPrintListener(ConfirmMsgRL)); + + this.add(m_ConfirmMsgLabel); + + m_OkButton = new Submit("OK"); + m_OkButton.setButtonLabel("OK"); + this.add(m_OkButton); + m_OkRL = OkUrlRL; + + m_CancelButton = new Submit("Cancel"); + m_CancelButton.setButtonLabel("Cancel"); + this.add(m_CancelButton); + m_CancelRL = CancelUrlRL; + + this.addInitListener(this); + this.addProcessListener(this); + } + + public void init(FormSectionEvent e) { + PageState ps = e.getPageState(); + + //initialize the OK and Cancel URL's + m_sOkUrl = (String) m_OkRL.get(ps); + m_sCancelUrl = (String) m_CancelRL.get(ps); + } + + public void process(FormSectionEvent e) { + PageState ps = e.getPageState(); + + if (m_OkButton.isSelected(ps)) { + throw new RedirectSignal(m_sOkUrl, true); + } else { + throw new RedirectSignal(m_sCancelUrl, false); + } + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Container.java b/ccm-core/src/main/java/com/arsdigita/bebop/Container.java new file mode 100755 index 000000000..4b81b5cdb --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/Container.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + + +/** + * The common interface that is implemented by all Bebop containers. + * + * The Container interface extends the Component interface. A container is + * simply a component that contains other components. + * + * @author David Lutterkort + * @author Uday Mathur + * @version $Id: Container.java 287 2005-02-22 00:29:02Z sskracic $ + * */ +public interface Container extends Component { + + /** + * Adds a component to this container. + * + * @param pc component to add to this container + * @pre pc != null + */ + void add(Component pc); + + /** + * Adds a component with the specified layout constraints to this + * container. Layout constraints are defined in each layout container as + * static ints. To specify multiple constraints, uses bitwise OR. + * + * @param pc component to add to this container + * + * @param constraints layout constraints (a + * bitwise OR of static ints in the particular layout) + * + * @pre c != null + */ + void add(Component c, int constraints); + + /** + * Returns true if this list contains the specified element. + * More formally, returns true if and only if this list + * contains at least + * one element e such that (o==null ? e==null : o.equals(e)). + *

+ * This method returns true only if the object has been + * directly added to this container. If this container contains another + * container that contains this object, this method returns + * false. + * + * @param o element whose presence in this container is to be tested + * + * @return true if this container contains the specified + * object directly; false otherwise. + * @pre o != null + */ + boolean contains(Object o); + + /** + * Gets the component + * at the specified position. Each call to the add method increments + * the index. Since the user has no control over the index of added + * components (other than counting each call to the add method), + * this method should be used in conjunction with indexOf. + * + * @param index the index of the item to be retrieved from this + * container + * + * @return the component at the specified position in this container. + * + * @pre index >= 0 && index < size() + * @post return != null */ + Component get(int index); + + /** + * + * + * @param pc component to search for + * + * @return the index in this list of the first occurrence of + * the specified element, or -1 if this list does not contain this + * element. + * + * @pre pc != null + * @post contains(pc) implies (return >= 0 && return < size()) + * @post ! contains(pc) implies return == -1 + */ + int indexOf(Component pc); + + /** + * Returns true if the container contains no components. + * + * @return true if this container contains no components; + * false otherwise. + * @post return == ( size() == 0 ) + */ + boolean isEmpty(); + + /** + * Returns the number of elements in this container. This does not + * recursively count components that are indirectly contained in this container. + * + * @return the number of components directly in this container. + * @post size() >= 0 + */ + int size(); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/ControlLink.java b/ccm-core/src/main/java/com/arsdigita/bebop/ControlLink.java new file mode 100755 index 000000000..0e844ae1e --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/ControlLink.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import java.io.IOException; +import java.util.ArrayList; + +import com.arsdigita.bebop.event.ActionEvent; +import com.arsdigita.bebop.event.ActionListener; +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Element; + +/** + * A link back to the page in which it is contained. The control link captures + * and preserves the current state of the page, and possibly any control events + * that have been set. It is most useful inside a {@link + * com.arsdigita.bebop.list.ListCellRenderer} or a {@link + * com.arsdigita.bebop.table.TableCellRenderer}, where the list or table has + * already set up the events 'tight' for the control link to do the right thing. + * + *

Warning: Even though a control link lets you add action + * listeners, they are not run unless you override {@link #setControlEvent + * setControlEvent}. If you need this behavior, you should use an {@link + * ActionLink}. A control link is hardly ever useful unless it is contained + * in an event-generating component like {@link List} or {@link Table}. + * + *

Example: A control link is mainly useful to send events to other + * components. For example, the following control link will cause a control + * event delete with associated value 42 to be sent to + * the component fooComponent when the user clicks on it: + * + *

+ *    ControlLink l = new ControlLink("click here") {
+ *      public void setControlEvent(PageState s) {
+ *        s.setControlEvent(fooComponent, "delete", 42);
+ *      }
+ *    };
+ * 
+ * + *

This requires that fooComponent is part of the page + * hierarchy. The control link l does not have to be part of the + * page hierarchy, and may be generated on the fly. (See + * {@link PageState} for details on control events.) + * + *

See {@link BaseLink} for a description + * of all Bebop Link classes. + * + * @author Stanislav Freidin + * @author David Lutterkort + * @version $Id: ControlLink.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class ControlLink extends BaseLink { + + /** + * The XML type attribute for a {@link ControlLink}. + */ + protected final String TYPE_CONTROL = "control"; + + /** + * A list of all action listeners. The list is instantiated lazily, and + * will therefore be null in most applications. + */ + private ArrayList m_actionListeners; + + /** + * Constructs a new ControlLink. The link will encapsulates + * the child component (which should be a label or an image). + * + * @param child the component that will be turned into a link + */ + public ControlLink(Component child) { + super(child, ""); + setTypeAttr(TYPE_CONTROL); + } + + /** + * Constructs a new ControlLink with the given string label. + * + * @param label the string label for the link + */ + public ControlLink(String label) { + this(new Label(label)); + } + + /** + * Adds an ActionListener, which will be run when {@link + * #respond respond} is called. + * @param 1 a listener to add + * + * @pre l != null + * @pre ! isLocked() + * @see #respond respond + */ + public void addActionListener(ActionListener l) { + Assert.isUnlocked(this); + if ( m_actionListeners == null ) { + m_actionListeners = new ArrayList(); + } + m_actionListeners.add(l); + } + + /** + * Removes a previously added ActionListener. + * @param 1 the listener to remove + * @see #addActionListener addActionListener + */ + public void removeActionListener(ActionListener l) { + Assert.isUnlocked(this); + if ( m_actionListeners == null ) { + return; + } + m_actionListeners.remove(l); + } + + /** + * Fires an ActionEvent, which causes all registered + * ActionListeners to be run. The source of the event + * is the TabbedPane. + * @param state the current page state + * @pre state != null + * @see #respond respond + */ + protected void fireActionEvent(PageState state) { + ActionEvent e = null; + if (m_actionListeners == null) { + return; + } + for (int i=0; i < m_actionListeners.size(); i++ ) { + if ( e == null ) { + e = new ActionEvent(this, state); + } + ((ActionListener) m_actionListeners.get(i)).actionPerformed(e); + } + } + + /** + * Responds to the incoming request. Fires the ActionEvent. + * @param state the current page state + */ + @Override + public void respond(PageState state) { + fireActionEvent(state); + } + + /** + * Generates the URL for a link and sets it as the "href" attribute + * of the parent. + * + * @param state the current page state + * @param parent the parent element + */ + @Override + protected void generateURL(PageState state, Element parent) { + setControlEvent(state); + try { + parent.addAttribute("href", state.stateAsURL()); + } catch (IOException e) { + parent.addAttribute("href", ""); + } + exportAttributes(parent); + state.clearControlEvent(); + } + + /** + * Sets the page state's control event. Should be overridden by child + * classes. By default, the link receives no control events whatsoever. + * + * @param ps the current page state + */ + // FIXME: Why is this not protected ? + public void setControlEvent(PageState ps) { + return; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/DescriptiveComponent.java b/ccm-core/src/main/java/com/arsdigita/bebop/DescriptiveComponent.java new file mode 100755 index 000000000..9efd421bb --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/DescriptiveComponent.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.globalization.GlobalizedMessage; +import com.arsdigita.xml.Element; + +import org.apache.log4j.Logger; + + +/** + * A (Simple) Component with various descriptive information, specifically 'hints' + * with explanations about it's proper usage. These hints provide a kind of + * online manual. + * + * @author Peter Boy (pb@zes.uni-bremen.de) + * @version $Id: TextStylable.java 287 2005-02-22 00:29:02Z sskracic $ + */ +abstract public class DescriptiveComponent extends SimpleComponent { + + /** Internal logger instance to faciliate debugging. Enable logging output + * by editing /WEB-INF/conf/log4j.properties int the runtime environment + * and set com.arsdigita.bebop.DescriptiveComponent=DEBUG + * by uncommenting or adding the line. */ + private static final Logger s_log = Logger.getLogger(DescriptiveComponent.class); + + /** Property to store informational text for the user about the Link, e.g. + * how to use it, or when to use it (or not to use it). */ + private GlobalizedMessage m_hint; //= GlobalizationUtil.globalize("bebop.hint.no_entry_yet"); + + /** Property to store a (localized) label (or title) of this widget. A + * label is the text (name) displayed for the user to identify and + * distinguish the various elements on the screem. */ + private GlobalizedMessage m_label; + + /** + * Sets a popup hint for the component. It usually contains some explanation + * for the user about the component, how to use, why it is there, etc. + * + * @param hint GlobalizedMessage object with the information text. + */ + public void setHint(GlobalizedMessage hint) { + m_hint = hint; + } + + /** + * Retrieve the popup hint for the component. It is specifically meant for + * client classes which have to generate the xml on their own and can not + * use the generateDescriptionXML method provided. + * + * @return popup hint message for the component + */ + public GlobalizedMessage getHint() { + return m_hint; + } + + /** + * Sets a popup hint for the Link. It usually contains some explanation for + * the user about the link, how to use, why it is there, etc. + * + * @param label GlobalizedMessage object with the text to identify and + * distinguish the component. + */ + public void setLabel(GlobalizedMessage label) { + m_label = label; + } + + /** + * Retrieve the label for the component. It is specifically meant for + * client classes which have to generate the XML on their own and can not + * use the generateDescriptionXML method provided. + * + * @return popup hint message for the component + */ + public GlobalizedMessage getLabel() { + return m_label; + } + + /** + * Generates a (J)DOM fragment for clients to include into their generated + * XML. + * + * @param state + * @param parent the XML Element instance to add the attributes managed by + * by this class + */ + protected void generateDescriptionXML(final PageState state, + final Element parent) { + + if (m_label != null) { + parent.addAttribute("label", (String) m_label.localize()); + } + if (m_hint != null) { + parent.addAttribute("hint", (String) m_hint.localize()); + } + // Do we need this? + //exportAttributes(parent); + } + + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Form.java b/ccm-core/src/main/java/com/arsdigita/bebop/Form.java new file mode 100755 index 000000000..392239323 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/Form.java @@ -0,0 +1,558 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.form.Hidden; +import javax.servlet.ServletException; +import com.arsdigita.bebop.util.Traversal; +import com.arsdigita.bebop.util.BebopConstants; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.web.URL; +import com.arsdigita.web.Web; +import com.arsdigita.web.RedirectSignal; +import com.arsdigita.util.Assert; +import com.arsdigita.util.UncheckedWrapperException; +import com.arsdigita.xml.Element; +import com.arsdigita.globalization.GlobalizedMessage; + +import java.util.Iterator; + +import org.apache.log4j.Logger; + +/** + * Represents the visual structure of an HTML form. Forms can be constructed with a Container + * argument to specify the type of layout this form will adhere to. The default is a column panel. + * + *

+ * As an example, a form that accepts a first and last name may be set up as follows: + * + *

+ * public class MyForm extends Form implements FormProcessListener {
+ *
+ * private Text m_firstName; private Text m_lastName;
+ *
+ * public MyForm() { super("myform"); add(new Label("First Name:")); m_firstName = new
+ * Text("firstName"); m_firstName.setDefaultValue("John"); add(m_firstName);
+ *
+ * add(new Label("Last Name:")); m_lastName = new Text("lastName");
+ * m_lastName.setDefaultValue("Doe"); m_lastName.addValidationListener(new NotNullValidationListener
+ * ("The last name")); add(m_lastName);
+ *
+ * add(new Submit("save", "Save")); addProcessListener(this); }
+ *
+ * public void process(FormSectionEvent e) { PageState s = e.getPageState();
+ *
+ * System.out.println("You are " + m_firstName.getValue(s) + " " + m_lastName.getValue(s)); } }
+ * 
+ * + *

+ * This form automatically checks that the user supplied a last name. Only then does it call the + * process method, which prints the user-supplied values. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Stas Freidin + * @author Rory Solomon + * @author David Lutterkort + * + * @version $Id: Form.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class Form extends FormSection implements BebopConstants { + + /** + * Internal logger instance to faciliate debugging. Enable logging output by editing + * /WEB-INF/conf/log4j.properties int hte runtime environment and set + * com.arsdigita.bebop.Form=DEBUG by uncommenting or adding the line. + */ + private static final Logger s_log = Logger.getLogger(Form.class); + + /** + * Constant for specifying a get submission method for this form. See the W3C HTML specification for + * a description of what this attribute does. + */ + public final static String GET = "get"; + + /** + * Constant for specifying a post submission method for this form. See the W3C HTML specification for + * a description of what this attribute does. + */ + public final static String POST = "post"; + + /** + * The name of the name attribute for the form. + */ + private final static String NAME = "name"; + + /** + * The name of the method attribute for the form. + */ + private final static String METHOD = "method"; + + private String m_action; + private boolean m_processInvisible; + + /** + * Hold the FormData for one request. + */ + private RequestLocal m_formData; + + /** + * Determines whether or not a form is 'redirecting', meaning that it will clear the control + * event and redirect to the resulting state after form processing, so that a page reload won't + * cause the form to be resubmitted. + */ + private boolean m_isRedirecting = false; + + /** + * Constructs a new form with the specified name. At the time of creation, instantiates a new + * form model for the form and instantiates a default ColumnPanel to contain the components. + * + * @param name the name of the form + */ + public Form(String name) { + this(name, new GridPanel(2)); + } + + /** + * Constructs a new form with the specified name and container. At the time of creation, + * instantiates a new form model for the form and replaces the default ColumnPanel with the + * specified container as the implicit container of the components. + * + * @param name the name attribute of the form + * @param panel the implicit container that will hold the components + */ + public Form(String name, Container panel) { + super(panel, new FormModel(name)); + initFormData(); + setName(name); + setProcessInvisible(false); + addMagicTag(); + } + + /** + * Writes the output to a DOM to be used with the XSLT template to produce the appropriate + * output. If the form is not visible, no output is generated. + * + *

+ * Generates a DOM fragment: + *

+ *

+     * <bebop:form action=%url; %bebopAttr;>
+     *   .. XML for panel ..
+     *   .. XML for page state ..
+     * </bebop:form>
+     * 
+ * + * @param s the page state used to determine the values of form widgets and page state + * attributes + * @param parent the XML element to which the form adds its XML representation + * + * @see PageState#generateXML + */ + @Override + public void generateXML(PageState s, Element parent) { + if (isVisible(s)) { + Element form = generateXMLSansState(s, parent); + + s.setControlEvent(this); + s.generateXML(form, getModel().getParametersToExclude()); + s.clearControlEvent(); + } + } + + /** + * Generates the XML representing the form and its widgets, but not the state information from + * s. + * + * @param s represents the curent request + * @param parent + * + * @return the top-level element for the form + */ + protected Element generateXMLSansState(PageState s, Element parent) { + Element form = parent.newChildElement("bebop:form", BEBOP_XML_NS); + + // Encode the URL with the servlet session information; + // do not use DispatcherHelper.encodeURL because the + // ACS global parameters are provided via the FormData. + String url = null; + + if (m_action == null) { + final URL requestURL = Web.getWebContext().getRequestURL(); + + if (requestURL == null) { + url = s.getRequest().getRequestURI(); + } else { + url = requestURL.getRequestURI(); + } + } else { + url = m_action; + } + + form.addAttribute("action", s.getResponse().encodeURL(url)); + + exportAttributes(form); + + m_panel.generateXML(s, form); + + generateErrors(s, form); + + return form; + } + + /** + * + * @param ps + * @param parent + */ + protected void generateErrors(PageState ps, Element parent) { + + for (Iterator it = getFormData(ps).getErrors(); it.hasNext();) { + Element errors = parent.newChildElement(BEBOP_FORMERRORS, + BEBOP_XML_NS); + Object msg = it.next(); + + if (msg == null) { + errors.addAttribute("message", "Unknown error"); + } else { + errors.addAttribute("message", + (String) ((GlobalizedMessage) msg).localize(ps.getRequest())); + } + errors.addAttribute("id", getName()); + } + + } + + /** + *

+ * Determine whether or not this Form will redirect after its process listeners are fired.

+ * + * @return + */ + public boolean isRedirecting() { + return m_isRedirecting; + } + + /** + * Setting the redirecting flag will cause the Form to clear the control event and redirect back + * to the current URL, after firing all process listeners. Doing so means that a user reload + * will not cause the form to be resubmitted. The default value for this flag is false. + * + * @param isRedirecting + */ + public void setRedirecting(boolean isRedirecting) { + Assert.isUnlocked(this); + m_isRedirecting = isRedirecting; + } + + /** + * Responds to the request by processing this form with the HTTP request given in + * state. + * + * @see #process process(...) + * + * @param state represents the current request + * + * @throws javax.servlet.ServletException + */ + @Override + public void respond(PageState state) throws ServletException { + final FormData data = process(state); + + if (m_isRedirecting && data.isValid()) { + state.clearControlEvent(); + + throw new RedirectSignal(state.toURL(), true); + } + } + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // Methods to set the HTML attributes of the FORM element + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + /** + * Sets the name attribute for the form. + * + * @param name the name for the form + * + * @pre ! isLocked() + */ + public void setName(String name) { + Assert.isUnlocked(this); + setAttribute(NAME, name); + } + + /** + * Gets the name attribute for this form. + * + * @return the name for this form. + */ + public String getName() { + return (String) getAttribute(NAME); + } + + /** + * Sets the enctype attribute used in the form element. No encoding + * type is specified by default. + * + * @param encType the encoding type + * + * @pre ! isLocked() + */ + public void setEncType(String encType) { + Assert.isUnlocked(this); + setAttribute("enctype", encType); + } + + /** + * Sets the onSubmit attribute used in the form element. No onsubmit + * handler is specified by default. + * + * @param javascriptCode the javascript code associated with this attribute + * + * @pre ! isLocked() + */ + public void setOnSubmit(String javascriptCode) { + Assert.isUnlocked(this); + setAttribute("onSubmit", javascriptCode); + } + + /** + * Sets the ONRESET attribute used in the FORM element. No onreset + * handler is specified by default. + * + * @param javascriptCode the javascript code associated with this attribute + * + * @pre ! isLocked() + */ + public void setOnReset(String javascriptCode) { + Assert.isUnlocked(this); + setAttribute("onReset", javascriptCode); + } + + /** + * Sets the HTTP method used to submit the form. + * + * @param method either GET or POST + * + * @pre ! isLocked() + */ + public void setMethod(String method) { + Assert.isUnlocked(this); + setAttribute(METHOD, method); + } + + private String getMethod() { + return getAttribute(METHOD); + } + + /** + * Returns true if form processing is turned on when the form is invisible. + * + * @return true if the form listeners should be processed even when the form is not visible on + * the page, false otherwise + */ + protected boolean getProcessInvisible() { + return m_processInvisible; + } + + /** + * Turns form processing on/off when the form is invisible. + * + * @param processInvisible true if the form listeners should be processed even when the form is + * not visible on the page + */ + protected void setProcessInvisible(boolean processInvisible) { + m_processInvisible = processInvisible; + } + + /** + * Sets the URL for the form's action attribute. This is the URL to which + * submissions will be sent when the user clicks a submit button on the form. By default, the + * action is null, instructing the form to set the action to the URL of the page in + * which it is used. If the action is set to a different URL, none of the listeners registered + * with this form will be run. + * + * @param action the URL to submit this form to + * + * @pre ! isLocked() + */ + public void setAction(String action) { + Assert.isUnlocked(this); + m_action = action; + } + + /** + * Returns the URL for the form's action attribute. + * + * @return the URL to which to submit this form. + * + * @see #setAction setAction + */ + public final String getAction() { + return m_action; + } + + /** + * Processes this form, creating a FormData object. Runs the right set of init, + * validation, and process listeners, depending on whether this is an initial request to the + * form and whether the form submission was valid. Submission listeners are always run. + * + * @see #getFormData + * + * @param state represents the current request + * + * @return the values extracted from the HTTP request contained in state. + * + * @throws com.arsdigita.bebop.FormProcessException + * @pre state != null + * @post return != null + */ + @Override + public FormData process(PageState state) throws FormProcessException { + Assert.exists(state, "PageState"); + FormData result = new FormData(getModel(), state.getRequest()); + setFormData(state, result); + + // Unless invisible form processing is turned on, don't run any + // listeners if this form is not visible. + if (getProcessInvisible() || state.isVisibleOnPage(this)) { + getModel().process(state, result); + } + return result; + } + + /** + * Returns the form data constructed by the {@link #process + * process} method for the request described by state. Processes the form if it has + * not already been processed. + * + * @param state describes the current request + * + * @return the values extracted from the HTTP request contained in state, or + * null if the form has not been processed yet. + * + * @pre state != null + * @post return != null + */ + public FormData getFormData(PageState state) { + return (FormData) m_formData.get(state); + } + + /** + * Adds a Hidden Tag to this form so that our controller can determine if this is an initial + * request. + */ + protected void addMagicTag() { + Hidden h = new Hidden(getModel().getMagicTagName()); + h.setDefaultValue("visited"); + add(h); + } + + /** + * Traverses the components contained in this form, collecting parameterModels and Listeners + * into this form's FormModel. + */ + protected void traverse() { + Traversal formRegistrar = new Traversal() { + + @Override + protected void act(Component c) { + if (c == Form.this) { + return; + } + if (c instanceof Form) { + throw new IllegalStateException("Forms cannot contain other Forms"); + } + c.register(Form.this, getModel()); + } + + }; + formRegistrar.preorder(this); + } + + /** + * Adds this form to the page and traverses the components contained in this form, collecting + * parameterModels and Listeners into this form's FormModel. + * + * @param p page in which to register this form + */ + @Override + public void register(Page p) { + traverse(); + p.addComponent(this); + } + + /** + * TODO + * + * @param model + */ + public void excludeParameterFromExport(ParameterModel model) { + getModel().excludeFormParameterFromExport(model); + } + + /** + * Initialize m_formData so that accessing the per-request form data forces the + * form to be processed on the first access and caches the form data for subsequent requests. + */ + private void initFormData() { + m_formData = new RequestLocal() { + + @Override + protected Object initialValue(PageState s) { + // TODO: We need to come up with the right strategy for + // how we deal with FormProcessExceptions. Are they fatal + // ? Do we just add them to the form validation errors ? + try { + return process(s); + } catch (FormProcessException e) { + s_log.error("Form Process exception", e); + throw new UncheckedWrapperException("Form Process error: " + + e.getMessage(), e); + } + } + + }; + } + + /** + * Converts to a String. + * + * @return a human-readable representation of this. + */ + @Override + public String toString() { + return super.toString() + " " + "[" + getName() + "," + getAction() + "," + getMethod() + + "," + isRedirecting() + "]"; + } + + /** + * Protected access to set the formdata request local. This method is required if a subclass + * wishes to override the process method. + * + * @param state + * @param data + */ + protected void setFormData(PageState state, FormData data) { + m_formData.set(state, data); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/FormData.java b/ccm-core/src/main/java/com/arsdigita/bebop/FormData.java new file mode 100755 index 000000000..0a1ccc694 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/FormData.java @@ -0,0 +1,832 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import com.arsdigita.bebop.parameters.ParameterData; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.util.Assert; +import com.arsdigita.globalization.GlobalizedMessage; +import com.arsdigita.util.URLRewriter; + +/** + * Manages the data associated with forms and other remote sources. + + *

The basic task of a FormData object is to transform + * a set of key-value string pairs into a validated set of Java data + * objects for use in subsequent processing. In most cases the original + * data is an HTTP request. + + *

To perform the transformation, a separate instance of + * FormModel is used to specify the name and basic data + * type of each expected parameter in the set, as well as any + * additional validation steps required. The FormData + * stores both the transformed data objects and any validation + * error messages associated with an individual parameter or the + * form as a whole. Once the data has been validated, individual data + * objects may be queried from a FormData using the + * standard get method of the Map interface. + * + *

FormData objects may also be used to control the + * entire lifecycle of self-validating forms, which report errors to + * the user in the context of the form itself, rather than on a + * separate page. + * + *

See the Forms API Developer Guide for details on using the + * FormData class. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Stas Freidin + * @version $Id: FormData.java 287 2005-02-22 00:29:02Z sskracic $ */ + +public class FormData implements Map, Cloneable { + + + private HashMap m_parameterDataValues = new HashMap(); + private LinkedList m_formErrors; + private FormModel m_model; + + private Locale m_locale; + private boolean m_isTransformed; + private boolean m_isValid; + + private boolean m_isSubmission; + + /** + * Ensure that no one can create this object from outside the package + * without supplying meaningful parameters + */ + private FormData() {} + + /** + * Constructs a new FormData object containing the + * transformed and validated query parameters from an HTTP request. + * + * @param model a FormModel describing the parameters + * and validation constraints for this request + * + * @param request an HTTP request object passed from the servlet + * container + * + * @pre model != null + * @pre request != null + * + * @throws FormProcessException if an error occurs. + */ + + public FormData(FormModel model, HttpServletRequest request) + throws FormProcessException { + + this(model, request, Locale.getDefault()); + } + + /** + * Constructs a new FormData object containing the + * transformed and validated query parameters from an HTTP request. + * + * @param model a FormModel describing the parameters + * and validation constraints for this request + * + * @param request an HTTP request object passed from the servlet + * container + * + * @param isSubmission true if the request should be treated + * as a form submission by the user + * + * @pre model != null + * @pre request != null + * + * @throws FormProcessException if an error occurs. + */ + public FormData(FormModel model, HttpServletRequest request, + boolean isSubmission) + throws FormProcessException { + + this(model, request, Locale.getDefault(), isSubmission); + } + + /** + * Constructs a new FormData object containing the + * transformed and validated query parameters from an HTTP request. + * + * @param model a FormModel describing the parameters + * and validation constraints for this request + * + * @param request an HTTP request object passed from the servlet + * container + * + * @param isSubmission true if the request should be treated + * as a form submission by the user + * + * @param fallback a fallback FormData object. If a value for + * a parameter in the form model model is not in + * the current request parameters, the fallback object is + * searched. + * + * @pre model != null + * @pre request != null + * + * @throws FormProcessException if an error occurs + */ + public FormData(FormModel model, HttpServletRequest request, + boolean isSubmission, FormData fallback) + throws FormProcessException { + + this(model, request, Locale.getDefault(), isSubmission, fallback); + } + + /** + * Constructs a new FormData object containing the + * transformed and validated query parameters from an HTTP request. + * Error messages are provided in the specified locale. + * + * @param model A FormModel describing the parameters + * and validation constraints for this request. + * + * @param request An HTTP request object passed from the servlet + * container. + * + * @param locale The locale for which all error messages will be + * prepared. This may be used in a multilingual environment to + * tailor the output to the preferences or geographic location of + * individual users on a per-request basis. + * + * @pre model != null + * @pre request != null + * @pre locale != null + * + * @throws FormProcessException if an error occurs + */ + public FormData(FormModel model, HttpServletRequest request, Locale locale) + throws FormProcessException { + this(model, request, locale, + request.getParameter(model.getMagicTagName()) != null); + } + + /** + * Constructs a new FormData object containing the + * transformed and validated query parameters from an HTTP request. + * Error messages are provided in the specified locale. + * + * @param model A FormModel describing the parameters + * and validation constraints for this request. + * + * @param request An HTTP request object passed from the servlet + * container. + * + * @param locale The locale for which all error messages will be + * prepared. This may be used in a multilingual environment to + * tailor the output to the preferences or geographic location of + * individual users on a per-request basis. + * + * @param isSubmission true if the request should be treated + * as a form submission by the user. + * + * @throws FormProcessException if an error occurs + * @pre model != null + * @pre request != null + * @pre locale != null + */ + public FormData(FormModel model, HttpServletRequest request, + Locale locale, boolean isSubmission) + throws FormProcessException { + this(model, request, locale, isSubmission, null); + } + + + /** + * Constructs a new FormData object containing the + * transformed and validated query parameters from an HTTP request. + * Error messages are provided in the specified locale. + * + * @param model A FormModel describing the parameters + * and validation constraints for this request. + * + * @param request An HTTP request object passed from the servlet + * container. + * + * @param locale The locale for which all error messages will be + * prepared. This may be used in a multilingual environment to + * tailor the output to the preferences or geographic location of + * individual users on a per-request basis. + * + * @param isSubmission true if the request should be treated + * as a form submission by the user. + * + * @param fallback a fallback FormData object. If a value for + * a parameter in the form model model is not in + * the current request parameters, the fallback object is + * searched. + * + * @throws FormProcessException if an error occurs + * @pre model != null + * @pre request != null + * @pre locale != null + */ + public FormData(FormModel model, HttpServletRequest request, + Locale locale, boolean isSubmission, + FormData fallback) + throws FormProcessException { + + Assert.exists(model, "FormModel"); + Assert.exists(request, "HttpServletRequest"); + Assert.exists(locale, "Locale"); + + m_locale = locale; + m_model = model; + m_isTransformed = false; + + m_isSubmission = isSubmission; + m_isValid = m_isSubmission; + + createParameterData(request, fallback); + + Iterator params = URLRewriter.getGlobalParams(request).iterator(); + while (params.hasNext()) { + ParameterData param = (ParameterData)params.next(); + setParameter(param.getModel().getName(), param); + } + } + + /** + * Validates this FormData object according to its form model. + * If the FormData is already valid, does nothing. + * + * @param state describes the current page state + * @pre state != null + */ + public void validate(PageState state) { + + if (isValid()) { + return; + } + + m_isValid = true; + + if (m_formErrors != null) { + m_formErrors.clear(); + } + + m_model.validate(state, this); + } + + /** + * Validates this FormData object against its form model, + * regardless of whether the object is currently valid. + * + * @param state describes the current page state + * @pre state != null + */ + public void forceValidate(PageState state) { + invalidate(); + validate(state); + } + + /** + * Reports a validation error on the form as a whole. + * + * @param message a String of the error message + * @pre message != null + * @deprecated refactor and use addError(GlobalizedMessage) instead + */ + public void addError(String message) { + addError(new GlobalizedMessage(message)); + } + + /** + * Reports a validation error on the form as a whole. + * Uses a GlobalizedMessage for inklusion + * + * @param message the error message + * @pre message != null + */ + public void addError(GlobalizedMessage message) { + + if (m_formErrors == null) { + m_formErrors = new LinkedList(); + } + + m_formErrors.add(message); + m_isValid = false; + } + + /** + * Adds an error message to the ParameterData object associated with + * the parameter model identified by name. + * + * @param name the name of the parameter model to whose + * ParameterData the error message will be added + * + * @param message the text of the error message to add + * + * @pre name != null + * @pre message != null + * @deprecated use addError(String name, GlobalizedMessage message) instead + */ + public void addError(String name, String message) { + + ParameterData parameter; + + if (!m_parameterDataValues.containsKey(name)) { + throw new IllegalArgumentException + ("Attempt to set Error in Non-Existant ParameterData"); + } + + parameter = (ParameterData) m_parameterDataValues.get(name); + parameter.addError(message); + m_isValid = false; + } + + + /** + * Adds an error message to the ParameterData object associated with + * the parameter model identified by name. + * + * @param name the name of the parameter model to whose + * ParameterData the error message will be added + * + * @param message the text of the error message to add + * + * @pre name != null + * @pre message != null + */ + public void addError(String name, GlobalizedMessage message) { + + ParameterData parameter; + + if (!m_parameterDataValues.containsKey(name)) { + throw new IllegalArgumentException + ("Attempt to set Error in Non-Existant ParameterData"); + } + + parameter = (ParameterData) m_parameterDataValues.get(name); + parameter.addError(message); + m_isValid = false; + } + + /** + * Returns the errors associated with the specified parameter. + * + * @param name the name of the parameter whose errors we are interested in + * + * @return an iterator of errors. Each error is just a string for + * now. + * + * @pre name != null + * @post return != null + */ + public Iterator getErrors(String name) { + + ParameterData parameter + = (ParameterData)m_parameterDataValues.get(name); + + if (parameter == null) { + return Collections.EMPTY_LIST.iterator(); + } + + return parameter.getErrors(); + } + + /** + * Returns an iterator over all the errors on this form that are not + * associated with any particular parameter. Such errors may have + * been generated by a FormValidationListener. + * + * @return an iterator over error messages. + * @post return != null + */ + public Iterator getErrors() { + + if (m_formErrors == null) { + return Collections.EMPTY_LIST.iterator(); + } + + return m_formErrors.iterator(); + } + + /** + * Returns an iterator over all of the errors on this form. + * This includes both errors associated with particular parameters + * and errors associated with the form as a whole. + * + * @return an iterator over all error messages. + * @post return != null + */ + public Iterator getAllErrors() { + + return new Iterator() { + + private Iterator params, paramErrors, formErrors; + + { + params = m_parameterDataValues.values().iterator(); + paramErrors = Collections.EMPTY_LIST.iterator(); + formErrors = getErrors(); + } + + private void seekToNextError() { + while (! paramErrors.hasNext() && params.hasNext()) { + paramErrors + = ((ParameterData)params.next()).getErrors(); + } + } + + @Override + public boolean hasNext() { + seekToNextError(); + return paramErrors.hasNext() || formErrors.hasNext(); + } + + @Override + public Object next() throws NoSuchElementException { + + seekToNextError(); + if (paramErrors.hasNext()) { + return paramErrors.next(); + } + + // An error will be thrown if !formErrors.hasNext() + return formErrors.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + /** + * Gets the specified ParameterData object. + * + * @param name the name of the parameterModel to retrieve + * @return the parameter data object specified. + * + */ + public ParameterData getParameter(String name) { + return (ParameterData)m_parameterDataValues.get(name); + } + + /** + * Sets the ParameterData object identified by the name in this FormData + * Object. + * + * @param name the name of the parameterModel + * @param value + */ + public void setParameter(String name, ParameterData value) { + m_parameterDataValues.put(name,value); + } + + /** + * Returns a collection of all the ParameterData objects. + * + * @return a collection of all the ParameterData objects. + */ + public final Collection getParameters() { + return m_parameterDataValues.values(); + } + + /** + * Determines whether this request represents a submission event. + * + * @return true if this request represents a submission event; + * false if it represents an initialization event. + */ + public final boolean isSubmission() { + return m_isSubmission; + } + + /** + * Determines whether the key-value string pairs in the + * request have been transformed into Java data objects. + * + * @return true if the key-value string pairs have been + * transformed into Java data objects; + * false otherwise. + * + */ + public final boolean isTransformed() { + return m_isTransformed; + } + + /** + * Determines whether any errors were found during validation of + * a form submission. + * @return true if no errors were found; false + * otherwise. + * + */ + public final boolean isValid() { + return m_isValid; + } + + /** + * Sets isValid to false. We do not allow programmers + * to manually toggle the isValid value to true. + * Hence this method takes no + * arguments and only sets isValid flag to false + * @deprecated Use invalidate() instead + */ + public void setInvalid() { + invalidate(); + } + + /** + * Set isValid to false. We do not allow programmers + * to manually toggle the isValid value to true. + */ + public final void invalidate() { + m_isValid = false; + } + + // --- private helper methods to initialize object --- + + /** + * Sets the value of a parameter within the associated ParameterData + * object + * + * @param name Name of the parameterModel whose ParameterData object + * we are setting + * + * @param value Value to assign the ParmeterData object + * + */ + private void setParameterValue(String name, Object value) { + ParameterData parameter = (ParameterData) m_parameterDataValues.get(name); + if (parameter != null) { + parameter.setValue(value); + } else { + throw new IllegalArgumentException("Parameter " + name + + " does not exist"); + } + } + + /** + * Iterate through parameterModels extracting values from the + * request, and transforming the value according to the parameter + * model This code incorporates + * ParameterModel.createParameterData(request) + * + * @param request the HttpServletRequest + * @param fallback a fallback FormData object. If any parameter + * in the form model does not have a value in the request, + * try to locate its value in the fallback object. + */ + private void createParameterData(HttpServletRequest request, + FormData fallback) + throws FormProcessException { + ParameterModel parameterModel; + ParameterData parameterData; + Iterator parameters = m_model.getParameters(); + + while (parameters.hasNext()) { + parameterModel = (ParameterModel) parameters.next(); + + // createParamterData automagically handles default values + // and errors in tranformation. + + Object defaultValue = null; + if (fallback != null) { + parameterData = + fallback.getParameter(parameterModel.getName()); + if (parameterData != null) { + defaultValue = parameterData.getValue(); + } + } + + // specify a default from the fallback + parameterData = + parameterModel.createParameterData(request, + defaultValue, + isSubmission()); + Assert.exists(parameterData); + setParameter(parameterModel.getName(), parameterData); + } + m_isTransformed=true; + } + + // --- Public methods to satisfy Map interface --- + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsKey(Object key) { + return m_parameterDataValues.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + // this is very expensive with ParameterData + throw new UnsupportedOperationException(); + } + + /* + * This is just plain wrong. Either you pretend to be a Map of + * things, or you are a Map of ParameterData-s. + */ + @Override + public Set entrySet() { + return m_parameterDataValues.entrySet(); + } + + /** + * Returns the value contained by the ParameterData object named + * by key. + * If no key is found, throws IllegalArgumentException. + * @param key the parameter data object to retrieve + * @return the value in the specified parameter data object. + * @throws java.lang.IllegalArgumentException thrown when the key + * is not a valid parameter. + */ + @Override + public Object get(Object key) throws IllegalArgumentException { + + ParameterData p = getParameter((String)key); + if (p != null) { + return p.getValue(); + } + throw new IllegalArgumentException("parameter " + key + + " not part of the form model"); + } + + /** + * @param m + * @return + * @deprecated Use get(m.getName()) instead, and then manually check + * for model identity + */ + public Object get(ParameterModel m) { + ParameterData p = getParameter(m.getName()); + + return ( p.getModel() == m ) ? p : null; + } + + + /** + * Retrieves a date object for the specified parameter name. + * @param key the object to retrieve + * @return a date object for the specified parameter name. + * + */ + public Date getDate(Object key) { + return (Date) get(key); + } + + /** + * Retrieves an integer object for the specified parameter name. + * @param key the object to retrieve + * @return an integer object for the specified parameter name. + **/ + + public Integer getInteger(Object key) { + return (Integer) get(key); + } + + /** + * Retrieves a String object for the specified parameter name. + * @param key the object to retrieve + * @return a string object for the specified parameter name. + **/ + + public String getString(Object key) { + return (String) get(key); + } + + + @Override + public boolean isEmpty() { + return m_parameterDataValues.isEmpty(); + } + + @Override + public Set keySet() { + return m_parameterDataValues.keySet(); + } + + @Override + public Object put(Object key, Object value) { + Object previousValue = get(key); + setParameterValue((String)key, value); + m_isValid = false; + return previousValue; + } + + @Override + public void putAll(Map t) { + for (Iterator i = t.keySet().iterator(); i.hasNext(); ) { + String key = (String) i.next(); + setParameterValue(key, t.get(key)); + } + m_isValid = false; + } + + /** + * + * @param key + * @return + */ + @Override + public Object remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + return m_parameterDataValues.size(); + } + + @Override + public Collection values() { + throw new UnsupportedOperationException(); + } + + /** + * + * @return + * @throws CloneNotSupportedException + */ + @Override + public Object clone() throws CloneNotSupportedException { + FormData result = (FormData) super.clone(); + result.m_parameterDataValues = new HashMap(); + for (Iterator i= m_parameterDataValues.keySet().iterator(); + i.hasNext(); ) { + Object key = i.next(); + ParameterData val = (ParameterData) m_parameterDataValues.get(key); + result.m_parameterDataValues.put(key, val.clone()); + } + if (m_formErrors != null) { + result.m_formErrors = (LinkedList) m_formErrors.clone(); + } + + return result; + } + + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + + for (Iterator i = getAllErrors(); i.hasNext();) { + s.append(i.next()).append(System.getProperty("line.separator")); + } + + return s.toString(); + } + + /** + * Converts to a String. + * The method {@link #toString()} returns all errors. + * + * @return a human-readable representation of this. + */ + public String asString() { + String newLine = System.getProperty("line.separator"); + StringBuilder to = new StringBuilder(); + to.append(super.toString() + " = {" + newLine); + //Map + to.append("m_parameterDataValues = ") + .append(m_parameterDataValues).append(",").append(newLine); + //LinkedList + to.append("m_formErrors = " + m_formErrors + "," + newLine); + //FormModel + to.append("m_model = " + m_model + "," + newLine); + to.append("m_locale = " + m_locale + "," + newLine); + to.append("m_isTransformed = " + m_isTransformed + "," + newLine); + to.append("m_isValid = " + m_isValid + "," + newLine); + to.append("m_isSubmission = " + m_isSubmission + newLine); + to.append("}"); + return to.toString(); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/FormModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/FormModel.java new file mode 100755 index 000000000..e5cb94e89 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/FormModel.java @@ -0,0 +1,550 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.event.EventListenerList; +import com.arsdigita.bebop.event.FormInitListener; +import com.arsdigita.bebop.event.FormProcessListener; +import com.arsdigita.bebop.event.FormSectionEvent; +import com.arsdigita.bebop.event.FormSubmissionListener; +import com.arsdigita.bebop.event.FormValidationListener; +import com.arsdigita.bebop.parameters.ParameterData; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.util.Assert; +import com.arsdigita.util.Lockable; +import com.arsdigita.util.URLRewriter; +import com.arsdigita.web.RedirectSignal; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import org.apache.log4j.Logger; + +/** + * A container for two classes of + * objects: ParameterModels and ValidationListeners.

+ *
    + *
  • ParameterModels are associated + * with the data objects that the user submits with the form.
  • + *
  • ValidationListeners provide custom + * cross-checking of parameter values.
  • + *
+ *

Instances of this class provide a specification for transforming a + * set of key-value string pairs into a set of validated Java data + * objects. + * A single instance of this + * class can handle all submissions to a particular form. + *

The most common usage for this class is + * is to use a private variable in a servlet to store the + * model, and to construct it in the servlet init method. + * That way, the model persists for the lifetime of the servlet, reducing + * the memory and processing overhead for each request. + *

See the + * Forms API Developer Guide for details on using the + * FormModel class. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Stas Freidin + * @author Rory Solomon + * @version $Id: FormModel.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class FormModel implements Lockable { + + private static final Logger s_log = Logger.getLogger(FormModel.class); + + private static final String MAGIC_TAG_PREFIX = "form."; + + private String m_name = null; + private List m_parameterModels = null; + private List m_parametersToExclude = null; + private boolean m_locked = false; + private boolean m_defaultOverridesNull; + + protected EventListenerList m_listenerList; + + /** + * Constructs a new form model. + * + * @param name a URL-encoded keyword used to identify this form model + * */ + public FormModel(String name) { + this(name, false); + } + + /** + * Construct a new form model. The defaultOverridesNull + * parameter is passed on to all parameter models that are added to the + * form model. If it is true, the parameter model will use + * the default value whenever it would normally set the parameter's value + * to null, for example if the parameter is missing from the request. If + * this value is false, the default parameter value will + * only be used if the request being processed is not a submission, but + * an initial request for the form model. + * + *

This method is only package-friendly since it is only useful to + * the Page class. Everybody else should be happy with the public + * constructor. + * + * @param name a URL-encoded keyword used to identify this form model + * + * @param defaultOverridesNull true if the default value for + * parameters should be used whenever the value would be + * null ordinarily. + */ + FormModel(String name, boolean defaultOverridesNull) { + Assert.exists(name, "Name"); + m_parameterModels = new LinkedList(); + m_parametersToExclude = new LinkedList(); + m_listenerList = new EventListenerList(); + m_name = name; + m_defaultOverridesNull = defaultOverridesNull; + m_parameterModels.addAll(URLRewriter.getGlobalModels()); + } + + /** + * Returns the name of this form model. + * + * @return a URL-encoded keyword used to identify requests + * conforming to this form model. + * */ + public final String getName() { + return m_name; + } + + public final void setName(String name) { + m_name = name; + } + + String getMagicTagName() { + return MAGIC_TAG_PREFIX + getName(); + } + + /** + * Adds a parameter model to the form model. The parameter model + * should be fully configured before adding it to the form model. + * + * @param parameter a parameter model object + * */ + public final void addFormParam(ParameterModel parameter) { + Assert.exists(parameter, "Parameter"); + Assert.isUnlocked(this); + parameter.setDefaultOverridesNull(m_defaultOverridesNull); + m_parameterModels.add(parameter); + + if( s_log.isDebugEnabled() ) { + s_log.debug( "Added parameter: " + parameter.getName() + "[" + + parameter.getClass().getName() + "]" ); + } + } + + + /** + * Adds a parameter model to the list of parameters that should + * not be exported when the form is rendered. Useful examples + * of this are for forms that loop back on themselves such as + * search forms or a control bar. The parameter model + * should be fully configured and have been added to the form model + * before adding it to the list of items to exclude + * + * @param parameter a parameter model object + * */ + public final void excludeFormParameterFromExport(ParameterModel parameter) { + Assert.exists(parameter, "Parameter"); + Assert.isUnlocked(this); + m_parametersToExclude.add(parameter); + } + + + /** + * Determines whether the form model contains the specified parameter + * model. + * @param p the parameter model to check for + * @return true if the form model contains the specified + * parameter model; false otherwise. + */ + public final boolean containsFormParam(ParameterModel p) { + Assert.exists(p, "Parameter"); + return m_parameterModels.contains(p); + } + + /** + * Returns an iterator over the parameter models contained within + * the form model. + * + * @return an iterator over the parameter models contained within + * the form model. + * */ + public final Iterator getParameters() { + return m_parameterModels.iterator(); + } + + + /** + * Returns an iterator over the parameter models that are + * contained within the form model but should not be exported + * as part of the form's state. This is important for situations + * where the form loops back on itself (e.g. a ControlBar or + * a Search form). + */ + public final Iterator getParametersToExclude() { + return m_parametersToExclude.iterator(); + } + + /** + * Adds a listener that is called as soon as the {@link FormData} has been + * initialized with the request parameters, but before any of the init, + * validation, or process listeners are run. The listener's + * submitted method may throw a + * FormProcessException to signal that any further + * processing of the form should be aborted. + * + * @param listener a FormSubmissionListener value + */ + public void addSubmissionListener(FormSubmissionListener listener) { + Assert.exists(listener, "Submission Listener"); + m_listenerList.add(FormSubmissionListener.class, listener); + } + + /** + * Adds a validation listener, implementing a custom validation + * check that applies to the form as a whole. Useful for checks + * that require examination of the values of more than one parameter. + * + * @param listener an instance of a class that implements the + * FormValidationListener interface + * */ + public void addValidationListener(FormValidationListener listener) { + Assert.exists(listener, "FormValidationListener"); + Assert.isUnlocked(this); + m_listenerList.add(FormValidationListener.class, listener); + } + + /** + * Adds a listener for form initialization events. + *

Initialization events occur when a form is initially + * requested by the user, but not when the form is subsequently + * submitted. They typically + * perform actions such as querying the database for existed values + * to set up an edit form or obtaining a sequence value to set up a + * create form. + * @param listener an instance of a class that implements the + * FormInitListener interface + * */ + public void addInitListener(FormInitListener listener) { + Assert.exists(listener, "FormInitListener"); + Assert.isUnlocked(this); + m_listenerList.add(FormInitListener.class, listener); + } + + /** + * Adds a listener for form processing events.

Process events + * only occur after a form submission has been successfully + * validated. They are typically used to perform a database + * transaction or other operation based on the submitted data. + *

Process listeners are executed in the order in which they are + * added. + * + * @param listener an instance of a class that implements the + * FormProcessListener interface + * */ + public void addProcessListener(FormProcessListener listener) { + Assert.exists(listener, "FormProcessListener"); + Assert.isUnlocked(this); + m_listenerList.add(FormProcessListener.class, listener); + } + + /** + * Creates a new FormData object that is populated with default values + * (for an initial request) or values from the request (for + * a submission). + *

If this is a submission, validates the data and (if the + * data is valid) calls the process listeners. Returns a FormData object. + * + * @param state the PageState object holding request-specific information + * @return a FormData object. + * */ + public FormData process(PageState state) throws FormProcessException { + Assert.isLocked(this); + boolean isSubmission = + state.getRequest().getParameter(getMagicTagName()) != null; + return process(state, isSubmission); + } + + /** + * Creates a new FormData object that is populated with default values + * (for an initial request) or values from the request (for a + * submission). + *

If this is a submission, validates the data and (if the + * data is valid) calls the process listeners. Returns a FormData object. + * + * @param state the PageState object holding request specific information + * @param isSubmission true if the request is a submission; + * false if this is the first request to the form data. + */ + public FormData process(PageState state, boolean isSubmission) + throws FormProcessException { + Assert.isLocked(this); + FormData data = new FormData(this, state.getRequest(), isSubmission); + try { + process(state, data); + } finally { + } + return data; + } + + + /** + * Do the work for the public process method. Uses the + * prepopulated FormData and runs listeners on it as + * needed. + * + * @throws FormProcessException if an error occurs + */ + void process(final PageState state, final FormData data) + throws FormProcessException { + s_log.debug("Processing the form model"); + + final FormSectionEvent e = new FormSectionEvent(this, state, data); + + if (data.isSubmission()) { + s_log.debug("The request is a form submission; running " + + "submission listeners"); + + try { + fireSubmitted(e); + } catch (FormProcessException fpe) { + s_log.debug("A FormProcessException was thrown while firing " + + "submit; aborting further processing"); + return; + } finally { + } + + + try { + s_log.debug("Validating parameters"); + fireParameterValidation(e); + + s_log.debug("Validating form"); + fireFormValidation(e); + } finally { + } + + if (data.isValid()) { + s_log.debug("The form data is valid; running process " + + "listeners"); + + try { + fireFormProcess(e); + } catch (FormProcessException fpe) { + s_log.debug("A FormProcessException was thrown while " + + "initializing the form; storing the error", fpe); + + data.addError("Initialization Aborted: " + fpe.getMessages()); + } finally { + } + } else { + s_log.debug("The form data was not valid; this form " + + "will not run its process listeners"); + } + } else { + s_log.debug("The request is not a form submission; " + + "running init listeners"); + + try { + fireFormInit(e); + } catch (FormProcessException fpe) { + s_log.debug("A FormProcessException was thrown while " + + "initializing the form; storing the error", fpe); + + data.addError("Initialization Aborted: " + fpe.getMessages()); + } finally { + } + } + } + + protected void fireSubmitted(FormSectionEvent e) + throws FormProcessException { + Assert.exists(e.getFormData(), "FormData"); + Assert.isLocked(this); + FormProcessException delayedException = null; + + Iterator i = m_listenerList.getListenerIterator(FormSubmissionListener.class); + while (i.hasNext()) { + try { + ((FormSubmissionListener) i.next()).submitted(e); + } catch (FormProcessException ex) { + delayedException = ex; + } + } + if ( delayedException != null ) { + throw delayedException; + } + } + + /** + * Calls a form initialization listener. + * + * @param e a FormSectionEvent originating from the form + */ + protected void fireFormInit(FormSectionEvent e) throws FormProcessException { + Assert.exists(e.getFormData(), "FormData"); + Assert.isLocked(this); + Iterator i = m_listenerList.getListenerIterator(FormInitListener.class); + while (i.hasNext()) { + ((FormInitListener) i.next()).init(e); + } + } + + /** + * Private helper method that validates the individual parameters by + * calling ParameterValidationListeners from the individual + * parameterModels. + * + * @param e a FormSectionEvent originating from the form + * */ + protected void fireParameterValidation(FormSectionEvent e) { + FormData data = e.getFormData(); + Assert.exists(data, "FormData"); + Iterator parameters = getParameters(); + ParameterModel parameterModel; + ParameterData parameterData; + while (parameters.hasNext()) { + parameterModel = (ParameterModel) parameters.next(); + parameterData = (ParameterData) data.getParameter(parameterModel.getName()); + try { + parameterData.validate(); + if (!parameterData.isValid()) { + data.invalidate(); + } + } catch (FormProcessException fpe) { + data.addError("Processing Listener Error: " + fpe.getMessage()); + } + } + } + + /** + * Private helper method. Validates the form by calling + * FormValidationListeners + * + * @param e a FormSectionEvent originating from the Form + * */ + private void fireFormValidation(FormSectionEvent e) { + FormData data = e.getFormData(); + Assert.exists(data, "FormData"); + Iterator i = m_listenerList.getListenerIterator(FormValidationListener.class); + while (i.hasNext()) { + try { + ((FormValidationListener) i.next()).validate(e); + } catch (FormProcessException fpe) { + data.addError(fpe.getMessage()); + } + } + } + + /** + * Call form process listeners.

Form processing is performed + * after the form has been validated.

+ * + * @param e a FormSectionEvent originating from the form + * */ + private void fireFormProcess(FormSectionEvent e) + throws FormProcessException { + Assert.exists(e.getFormData(), "FormData"); + if (!e.getFormData().isValid()) { + throw new IllegalStateException("Request data must be valid " + "prior to running processing filters."); + } + Iterator i = m_listenerList.getListenerIterator(FormProcessListener.class); + + RedirectSignal redirect = null; + while (i.hasNext()) { + try { + ((FormProcessListener) i.next()).process(e); + } catch( RedirectSignal signal ) { + if( s_log.isDebugEnabled() ) { + s_log.debug( "Delaying redirect to " + + signal.getDestinationURL() ); + } + + if( null != redirect ) { + s_log.error( "Non-deterministic redirect. Ignoring earlier occurrence.", redirect ); + } + + redirect = signal; + } + } + + if( null != redirect ) throw redirect; + } + + /** + * Call form validation listeners. Listeners that encounter + * validation errors report them directly to the + * FormData object.

Form validation is performed + * after the initial transformation of key-value string + * pairs into Java data objects is complete. + * + * @param state the page state for this request + * + * @param data the FormData object to validate + * + * @pre data != null + * */ + void validate(PageState state, FormData data) { + Assert.exists(data, "FormData"); + if (!data.isTransformed()) { + throw new IllegalStateException("Request data must be transformed " + "prior to running validation filters."); + } + fireParameterValidation(new FormSectionEvent(this, state, data)); + fireFormValidation(new FormSectionEvent(this, state, data)); + } + + /** + * Merge the parameterModels and Listeners from the supplied + * FormModel into the current FormModel. This method is useful when + * registering FormSections in Forms. + * + * @param m The FormModel to be merged into this FormModel + * */ + void mergeModel(FormModel m) { + Assert.isUnlocked(this); + Assert.exists(m, "FormSection's FormModel"); + m_parameterModels.addAll(m.m_parameterModels); + m_listenerList.addAll(m.m_listenerList); + } + + /** + * Locks this FormModel and all of its ParameterModels. + * */ + public void lock() { + for (Iterator i = getParameters(); i.hasNext(); ) { + ((ParameterModel) i.next()).lock(); + } + m_locked = true; + } + + /** + * Checks whether this FormModel is locked. + * + * @return true if this FormModel is locked; + * false otherwise. + * */ + public final boolean isLocked() { + return m_locked; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/FormProcessException.java b/ccm-core/src/main/java/com/arsdigita/bebop/FormProcessException.java new file mode 100644 index 000000000..bb73515cf --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/FormProcessException.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.globalization.GlobalizedMessage; +import javax.servlet.ServletException; + +/** + * This class represents exceptions that occur within the processing methods + * of any of the form event listeners. Typically the code will catch specific + * exceptions such as SQLException and rethrow them as instances + * of this class to pass the message to the controller in a standard fashion. + * + *

Since this class is a subclass of ServletException, servlets + * that do form processing within a doPost or doGet + * methods do not need to explicitly catch instances of this class. However, + * they may wish to do so for special error reporting to the user, or to notify + * the webmaster via e-mail of the problem. + * + * @version $Id$ + */ + +public class FormProcessException extends ServletException { + + /** Globalized version of the exception message, intended for output in the UI */ + private GlobalizedMessage m_globalizedMessage; + + /** + * Constructor using a String as message presented to the user. + * @param message + * @deprecated Use FormProcessException(GlobalizedMessage) instead. The + * error message for the user should always be globalized so it + * can be transformed to the current users requested language. + */ + public FormProcessException(String message) { + super(message); + } + + /** + * Constructor using both types of messages which may be presented to the + * user. It's a kind of fallback just in kind we really need a non- + * globalized message. Usage is stropngly discouraged. + * @param message + * @param globalizedMessage + */ + public FormProcessException(String message, + GlobalizedMessage globalizedMessage) { + super(message); + m_globalizedMessage = globalizedMessage; + } + + /** + * Constructor using a GlobalizedMessage as the error text presented to the + * user. Using this constructor is the strongly recommended way! + * + * @param globalizedMessage + */ + public FormProcessException(GlobalizedMessage globalizedMessage) { + super(); + m_globalizedMessage = globalizedMessage; + } + + /** + * + * @param message + * @param rootCause + * @deprecated use FormProcessException(String,GlobalizedMessage,Throwable) + * instead + */ + public FormProcessException(String message, + Throwable rootCause) { + super(message, rootCause); + } + + public FormProcessException(String message, + GlobalizedMessage globalizedMessage, + Throwable rootCause) { + super(message, rootCause); + m_globalizedMessage = globalizedMessage; + } + + public FormProcessException(Throwable rootCause) { + super(rootCause); + } + + /** + * Add a globalized version of the exception message just in case a non- + * globalized message enabled constructor has been used. + * + * @param globalizedMessage the globalized message intended for output in UI + */ + public void setGlobalizedMessage(GlobalizedMessage globalizedMessage) { + m_globalizedMessage = globalizedMessage; + } + + /** + * Retrieve the globalized version of the exception message, intended for + * use in the UI widgets. + * The standard non-globalizatin enabled exception message is for use in + * log entries only! + * + * @return the globalized message intended for output in UI + */ + GlobalizedMessage getGlobalizedMessage() { + return m_globalizedMessage; + } + /** + * In addition to printing the stack trace for this exception, also prints + * the stack trace for the root cause, if any. This is a workaround for + * those implementations of {@link ServletException} that don't implement + * printStackTrace correctly. If you happen to use an + * implementation that does, the stack trace for the root cause may be + * printed twice, which is not that big of a deal in the grand scheme of + * things. + */ + @Override + public void printStackTrace() { + super.printStackTrace(); + if (getRootCause() != null) { + System.err.print("Root cause: "); + getRootCause().printStackTrace(); + } + } + + /** + * @param s + * @see #printStackTrace() + */ + @Override + public void printStackTrace(java.io.PrintStream s) { + super.printStackTrace(s); + if (getRootCause() != null) { + s.println("Root cause: "); + getRootCause().printStackTrace(s); + } + } + + /** + * @param s + * @see #printStackTrace() + */ + @Override + public void printStackTrace(java.io.PrintWriter s) { + super.printStackTrace(s); + if (getRootCause() != null) { + s.println("Root cause: "); + getRootCause().printStackTrace(s); + } + } + + /** + *

Returns the concatenation of {@link #getMessage()} and {@link + * #getRootCause()}.getMessage().

+ * @return + **/ + public String getMessages() { + StringBuilder result = new StringBuilder(getMessage()); + if ( getRootCause() != null ) { + result.append(" (root cause: ") + .append(getRootCause().getMessage()) + .append(")"); + } + return result.toString(); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/FormSection.java b/ccm-core/src/main/java/com/arsdigita/bebop/FormSection.java new file mode 100755 index 000000000..f255a7a38 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/FormSection.java @@ -0,0 +1,777 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.event.EventListenerList; +import com.arsdigita.bebop.event.FormCancelListener; +import com.arsdigita.bebop.event.FormInitListener; +import com.arsdigita.bebop.event.FormProcessListener; +import com.arsdigita.bebop.event.FormSectionEvent; +import com.arsdigita.bebop.event.FormSubmissionListener; +import com.arsdigita.bebop.event.FormValidationListener; +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Element; + +import java.util.Iterator; + +import org.apache.log4j.Logger; + +/** + * A standalone section of a Form. A FormSection + * contains other Bebop components, most importantly + * Widgets and associated listeners. It serves two purposes: + *
    + *
  • Divides a form into visual sections
  • + *
  • Serves as a container for form fragments that can function by themselves + * and can be dropped into other forms
  • + *
+ *

Since a FormSection has its own init, validation, and + * process listeners, it can do all of its processing without any intervention + * from the enclosing form. + * + * Although a FormSection contains all the same pieces + * that a Form does, it can only be used if it is added + * directly or indirectly to a Form. FormSections + * that are not contained in a Form do not exhibit any useful + * behavior. + * + * @see Form + * @see FormModel + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Stas Freidin + * @author Rory Solomon + * @author David Lutterkort + * + * @version $Id: FormSection.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class FormSection extends DescriptiveComponent implements Container { + + /** Internal logger instance to faciliate debugging. Enable logging output + * by editing /WEB-INF/conf/log4j.properties int the runtime environment + * and set com.arsdigita.subsite.FormSection=DEBUG + * by uncommenting or adding the line. */ + private static final Logger s_log = Logger.getLogger(FormSection.class); + + /** Underlying FormModel that stores + * the parameter models for all the widgets in this form section. */ + protected FormModel m_formModel; + + /** The container to which all children are added. A + * ColumnPanel by default. */ + protected Container m_panel; + + /** Contains all the listeners that were added with the various + * addXXXListener methods. + * We maintain our own list of listeners, so that we can re-send the + * events the FormModel generates, but with us as the source, not the + * FormModel. */ + private EventListenerList m_listeners; + + /** Listeners we attach to the FormModel to forward + * form model events to our listeners with the right source */ + private FormSubmissionListener m_forwardSubmission; + + private FormInitListener m_forwardInit; + private FormValidationListener m_forwardValidation; + private FormProcessListener m_forwardProcess; + + /** + * Constructs a new form section. Sets the implicit layout Container of + * this FormSection to two column ColumnPanel + * by calling the 1-argument constructor. + **/ + public FormSection() { + this(new ColumnPanel(2, true)); + } + + /** + * Constructs a new form section. Sets the form model of this + * FormSection to a new, anonymous FormModel. + * + * @param panel + **/ + public FormSection(Container panel) { + this(panel, new FormModel("anonymous")); + } + + /** + * Constructs a new form section. Sets the implicit layout Container of + * this FormSection to panel. Sets the form + * model of this FormSection to model. + * + * @param panel the container within this form section that holds the + * components that are added to the form section with calls to the + * add methods + * + * @param model the form model for this form section + **/ + protected FormSection(Container panel, FormModel model) { + super(); + m_panel = panel; + m_formModel = model; + m_listeners = new EventListenerList(); + } + + /** + * Adds a listener that is called as soon as the {@link FormData} has been + * initialized with the request parameters but before any of the init, + * validation, or process listeners are run. The listener's + * submitted method may throw a + * FormProcessException to signal that any further + * processing of the form should be aborted. + * + * @param listener a submission listener to run every time the form is + * submitted + * @see FormModel#addSubmissionListener + * @pre listener != null + */ + public void addSubmissionListener(FormSubmissionListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Adding submission listener " + listener + " to " + this); + } + + Assert.exists(listener, "Submission Listener"); + Assert.isUnlocked(this); + forwardSubmission(); + m_listeners.add(FormSubmissionListener.class, listener); + } + + /** + * Removes the specified submission listener from the + * list of submission listeners (if it had previously been added). + * + * @param listener the submission listener to remove + */ + public void removeSubmissionListener(FormSubmissionListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Removing submission listener " + listener + " from " + + this); + } + + Assert.exists(listener, "Submission Listener"); + Assert.isUnlocked(this); + m_listeners.remove(FormSubmissionListener.class, listener); + } + + /** + * Calls the submitted method on all registered submission + * listeners. + * + * @param e the event to pass to the listeners + * @throws FormProcessException if one of the listeners throws such an + * exception. + */ + protected void fireSubmitted(FormSectionEvent e) + throws FormProcessException { + Assert.exists(e.getFormData(), "FormData"); + FormProcessException delayedException = null; + + Iterator i = m_listeners.getListenerIterator( + FormSubmissionListener.class); + while (i.hasNext()) { + final FormSubmissionListener listener = (FormSubmissionListener) i. + next(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Firing submission listener " + listener); + } + + try { + listener.submitted(e); + } catch (FormProcessException ex) { + s_log.debug(ex); + delayedException = ex; + } + } + if (delayedException != null) { + throw delayedException; + } + } + + /** + * + */ + protected void forwardSubmission() { + if (m_forwardSubmission == null) { + m_forwardSubmission = createSubmissionListener(); + getModel().addSubmissionListener(m_forwardSubmission); + } + } + + /** + * Creates the submission listener that forwards submission events to this + * form section. + * + * @return a submission listener that forwards submission events to this + * form section. + */ + protected FormSubmissionListener createSubmissionListener() { + return new FormSubmissionListener() { + + @Override + public void submitted(FormSectionEvent e) + throws FormProcessException { + fireSubmitted(new FormSectionEvent(FormSection.this, + e.getPageState(), + e.getFormData())); + } + }; + } + + /** + * Adds a listener for form initialization events. Initialization + * events occur when a form is initially requested by the user, but + * not when the form is subsequently submitted. They typically + * perform actions such as querying the database for existing values + * to set up an edit form, or obtaining a sequence value to set up a + * create form. + * + * @param listener an instance of a class that implements the + * FormInitListener interface + * */ + public void addInitListener(FormInitListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Adding init listener " + listener + " to " + this); + } + + Assert.exists(listener, "FormInitListener"); + Assert.isUnlocked(this); + forwardInit(); + m_listeners.add(FormInitListener.class, listener); + } + + /** + * Removes the specified init listener from the + * list of init listeners (if it had previously been added). + * + * @param listener the init listener to remove + */ + public void removeInitListener(FormInitListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Removing init listener " + listener + " from " + this); + } + + Assert.exists(listener, "Init Listener"); + Assert.isUnlocked(this); + m_listeners.remove(FormInitListener.class, listener); + } + + /** + * Calls the init method on all registered init + * listeners. + * + * @param e the event to pass to the listeners + * @throws FormProcessException if one of the listeners throws such an + * exception. + */ + protected void fireInit(FormSectionEvent e) throws FormProcessException { + Assert.exists(e.getFormData(), "FormData"); + Assert.isLocked(this); + Iterator i = m_listeners.getListenerIterator(FormInitListener.class); + while (i.hasNext()) { + final FormInitListener listener = (FormInitListener) i.next(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Firing init listener " + listener); + } + + listener.init(e); + } + } + + /** + * + */ + protected void forwardInit() { + if (m_forwardInit == null) { + m_forwardInit = createInitListener(); + getModel().addInitListener(m_forwardInit); + } + } + + /** + * Creates the init listener that forwards init events to this form + * section. + * + * @return an init listener that forwards init events to this + * form section. + */ + protected FormInitListener createInitListener() { + return new FormInitListener() { + + @Override + public void init(FormSectionEvent e) + throws FormProcessException { + fireInit(new FormSectionEvent(FormSection.this, + e.getPageState(), + e.getFormData())); + } + }; + } + + /** + * Creates the cancel listener that forwards cancel events to this form + * section + * + * @return an cancel listener + */ + protected FormCancelListener createCancelListener() { + return new FormCancelListener() { + + @Override + public void cancel(FormSectionEvent e) throws FormProcessException { + fireCancel(new FormSectionEvent(FormSection.this, + e.getPageState(), + e.getFormData())); + } + }; + } + + /** + * Adds a validation listener, implementing a custom validation + * check that applies to the form as a whole. Useful for checks + * that require examination of the values of more than one parameter. + * + * @param listener an instance of a class that implements the + * FormValidationListener interface + * */ + public void addValidationListener(FormValidationListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Adding validation listener " + listener + " to " + this); + } + + Assert.exists(listener, "FormValidationListener"); + Assert.isUnlocked(this); + forwardValidation(); + m_listeners.add(FormValidationListener.class, listener); + } + + /** + * Removes the specified validation listener from the + * list of validation listeners (if it had previously been added). + * + * @param listener a validation listener + */ + public void removeValidationListener(FormValidationListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Removing validation listener " + listener + " from " + + this); + } + + Assert.exists(listener, "Validation Listener"); + Assert.isUnlocked(this); + m_listeners.remove(FormValidationListener.class, listener); + } + + /** + * Calls the validate method on all registered validation + * listeners. + * + * @param e the event to pass to the listeners + */ + protected void fireValidate(FormSectionEvent e) { + FormData data = e.getFormData(); + Assert.exists(data, "FormData"); + Iterator i = m_listeners.getListenerIterator( + FormValidationListener.class); + while (i.hasNext()) { + try { + final FormValidationListener listener = + (FormValidationListener) i.next(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Firing validation listener " + listener); + } + + listener.validate(e); + } catch (FormProcessException fpe) { + s_log.debug(fpe); + data.addError(fpe.getGlobalizedMessage()); + } + } + } + + protected void forwardValidation() { + if (m_forwardValidation == null) { + m_forwardValidation = createValidationListener(); + getModel().addValidationListener(m_forwardValidation); + } + } + + /** + * Create the validation listener that forwards validation events to this + * form section. + * + * @return a validation listener that forwards validation events to this + * form section. + */ + protected FormValidationListener createValidationListener() { + return new FormValidationListener() { + + @Override + public void validate(FormSectionEvent e) { + fireValidate(new FormSectionEvent(FormSection.this, + e.getPageState(), + e.getFormData())); + } + }; + } + + /** + * Adds a listener for form processing events.

Process events + * only occur after a form submission has been successfully + * validated. They are typically used to perform a database + * transaction or other operation based on the submitted data. + *

Process listeners are executed in the order in which + * they are added. + * + * @param listener an instance of a class that implements the + * FormProcessListener interface + * */ + public void addProcessListener(final FormProcessListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Adding process listener " + listener + " to " + this); + } + + Assert.exists(listener, "FormProcessListener"); + Assert.isUnlocked(this); + + forwardProcess(); + m_listeners.add(FormProcessListener.class, listener); + } + + /** + * Removes the specified process listener from the + * list of process listeners (if it had previously been added). + * + * @param listener the process listener to remove + */ + public void removeProcessListener(FormProcessListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Removing process listener " + listener + " from " + + this); + } + + Assert.exists(listener, "Process Listener"); + Assert.isUnlocked(this); + + m_listeners.remove(FormProcessListener.class, listener); + } + + protected void forwardProcess() { + if (m_forwardProcess == null) { + m_forwardProcess = createProcessListener(); + getModel().addProcessListener(m_forwardProcess); + } + } + + protected FormProcessListener createProcessListener() { + return new FormProcessListener() { + + @Override + public void process(FormSectionEvent e) + throws FormProcessException { + fireProcess(new FormSectionEvent(FormSection.this, + e.getPageState(), + e.getFormData())); + } + }; + } + + /** + * Calls the process method on all registered process + * listeners. + * + * @param e the event to pass to the listeners + * @throws FormProcessException if one of the listeners throws such an + * exception. + */ + protected void fireProcess(FormSectionEvent e) + throws FormProcessException { + Assert.exists(e.getFormData(), "FormData"); + Iterator i = m_listeners.getListenerIterator(FormProcessListener.class); + while (i.hasNext()) { + final FormProcessListener listener = (FormProcessListener) i.next(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Firing process listener " + listener); + } + + listener.process(e); + } + } + + /** + * Since a form section cannot be processed, always throws an error. + * (Processing of form sections is done by the form in which the + * section is contained.) + * + * @param data + * @return + * @throws javax.servlet.ServletException because processing a form section + * is not meaningful. + */ + public FormData process(PageState data) + throws javax.servlet.ServletException { + throw new UnsupportedOperationException(); + } + + /** + * Adds a listener for form cancellation events. Cancellation + * listeners are typically used to clean-up page state and + * potentially intermediate changes to the database. + * + * @param listener an instance of a class that implements the + * FormCancelListener interface + * */ + public void addCancelListener(FormCancelListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Adding cancel listener " + listener + " to " + this); + } + + Assert.exists(listener, "FormCancelListener"); + Assert.isUnlocked(this); + m_listeners.add(FormCancelListener.class, listener); + } + + /** + * Removes the specified cancellation listener from the + * list of cancellation listeners (if it had previously been added). + * + * @param listener the cancellation listener to remove + */ + public void removeCancelListener(FormCancelListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Removing cancel listener " + listener + " from " + this); + } + + Assert.exists(listener, "Cancel Listener"); + Assert.isUnlocked(this); + m_listeners.remove(FormCancelListener.class, listener); + } + + /** + * Calls the cancel method on all registered cancellation + * listeners. + * + * @param e the event to pass to the listeners + * @throws FormProcessException if one of the listeners throws such an + * exception. + */ + protected void fireCancel(FormSectionEvent e) + throws FormProcessException { + Assert.exists(e.getFormData(), "FormData"); + Iterator i = m_listeners.getListenerIterator(FormCancelListener.class); + while (i.hasNext()) { + final FormCancelListener listener = (FormCancelListener) i.next(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Firing cancel listener " + listener); + } + + listener.cancel(e); + } + } + + /** + * Traverses the children this FormSection, collecting parameter models + * and listeners into the supplied FormModel. Sets implicit pointers + * of widgets in this FormSection to the supplied Form. + * + * @param f pointer to the form that is set inside Widgets within this + * FormSection + * @param m the FormModel in which to merge ParameterModels and + * Listeners + * */ + @Override + public void register(Form f, FormModel m) { + m.mergeModel(getModel()); + } + + /** + * Accessor method for this form's FormModel. + * + * @return FormModel The model of this form. + * */ + protected final FormModel getModel() { + return m_formModel; + } + + /** + * Locks this FormSection, its FormModel, and the implicit Container. + * */ + @Override + public void lock() { + m_formModel.lock(); + m_panel.lock(); + super.lock(); + } + + @Override + public void respond(PageState state) throws javax.servlet.ServletException { + //call listeners here. + throw new UnsupportedOperationException(); + } + + /** + * Returns the implicit Container of this FormSection. + * + * This must not be final, because MetaFrom needs to override it. + * + * @return + */ + public Container getPanel() { + return m_panel; + } + + /** + * Returns an iterator over the children of this component. If the + * component has no children, returns an empty iterator (not + * null !). + * + * @post return != null + * */ + @Override + public Iterator children() { + return m_panel.children(); + } + + /** + * Builds an XML subtree for this component under the specified + * parent. Uses the request values stored in + * state.

+ * + *

This method generates DOM to be used with the XSLT template + * to produce the appropriate output.

+ * + * @param pageState the state of the current page + * @param parent the node that will be used to write to + * */ + @Override + public void generateXML(PageState pageState, Element parent) { + if (isVisible(pageState)) { + m_panel.generateXML(pageState, parent); + } + } + + // Container methods + /** + * Adds a component to this container. + * + * @param pc the component to add to this container + * */ + @Override + public void add(Component pc) { + m_panel.add(pc); + } + + /** + * Adds a component with the specified layout constraints to this + * container. Layout constraints are defined in each layout container as + * static ints. Use a bitwise OR to specify multiple constraints. + * + * @param pc the component to add to this container + * @param constraints layout constraints (a bitwise OR of static ints in + * the particular layout) + */ + @Override + public void add(Component pc, int constraints) { + m_panel.add(pc, constraints); + } + + /** + * Returns true if this list contains the + * specified element. More + * formally, returns true if and only if this list contains at least + * one element e such that (o==null ? e==null : o.equals(e)). + * + * This method returns true only if the component has + * been directly + * added to this container. If this container contains another + * container that contains this component, this method returns + * false. + * + * @param o element whose presence in this container is to be tested + * + * @return true if this Container contains the + * specified component directly; false otherwise. + * + * */ + @Override + public boolean contains(Object o) { + return m_panel.contains(o); + } + + /** + * Returns the + * Component at the specified position. Each call to add() + * increments the index. This method should be used in conjunction + * with indexOf + * + * @param index The index of the item to be retrieved from this + * Container. Since the user has no control over the index of added + * components (other than counting each call to add), this method + * should be used in conjunction with indexOf. + * + * @return the component at the specified position in this container + * */ + @Override + public Component get(int index) { + return (Component) m_panel.get(index); + } + + /** + * + * + * + * @param pc component to search for + * + * @return the index in this list of the first occurrence of + * the specified element, or -1 if this list does not contain this + * element. + * */ + @Override + public int indexOf(Component pc) { + return m_panel.indexOf(pc); + } + + /** + * Determines whether the container contains any components. + * + * @return true if this container contains no components + * false otherwise. + * */ + @Override + public boolean isEmpty() { + return m_panel.isEmpty(); + } + + /** + * Returns the number of elements in this container. This does not + * recursively count the components indirectly contained in this container. + * + * @return the number of components directly in this container. + * */ + @Override + public int size() { + return m_panel.size(); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/FormStep.java b/ccm-core/src/main/java/com/arsdigita/bebop/FormStep.java new file mode 100755 index 000000000..a7dbcad81 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/FormStep.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import org.apache.log4j.Logger; + +import com.arsdigita.bebop.event.FormInitListener; +import com.arsdigita.bebop.event.FormSectionEvent; +import com.arsdigita.bebop.form.Widget; +import com.arsdigita.bebop.parameters.BooleanParameter; +import com.arsdigita.bebop.util.Traversal; +import com.arsdigita.xml.Element; + +/** + * The FormStep class modifies the behavior of FormSection with respect to + * listener firing. Instead of firing init listeners the first time the + * enclosing form is displayed on the page, the FormStep class fires init + * listeners the first time the FormStep itself is displayed on the page. The + * process, validate, and submission listeners are then fired on every + * submission following the one in which the init listeners were fired. This + * behavior is useful when used in conjunction with {@link MultiStepForm} or + * its subclasses to provide initialization in later steps of a multi step + * form that depends on the values entered in earlier steps. + * + * updated chris.gilbert@westsussex.gov.uk - support for session based wizards + * (which enable use of actionlinks in wizard) + + * @see Wizard + * @see MultiStepForm + * + * @author rhs@mit.edu + * @version $Id: FormStep.java 1414 2006-12-07 14:24:10Z chrisgilbert23 $ + **/ + +public class FormStep extends FormSection { + private final static Logger s_log = Logger.getLogger(FormStep.class); + + private Form m_form = null; + + // cg - changed to using a parameter that is stored in pagestate so that if there are links + // within the form then the init status of the steps is not lost + // private Hidden m_initialized; + + private BooleanParameter m_initialized; + + + /** + * Constructs a new FormStep with the given name. The name must uniquely + * identify this FormStep within it's enclosing Form. + * + * @param name A name that uniquely identifies this FormStep within it's + * enclosing Form. + **/ + + public FormStep(String name) { + addInitialized(name); + } + + /** + * Constructs a new FormStep with the given name. The name must uniquely + * identify this FormStep within it's enclosing Form. + * + * @param name A name that uniquely identifies this FormStep within it's + * enclosing Form. + * @param panel The container used to back this FormStep. + **/ + + public FormStep(String name, Container panel) { + super(panel); + addInitialized(name); + } + + protected FormStep(String name, Container panel, FormModel model) { + super(panel, model); + addInitialized(name); + } + + public void register(Page p) { + super.register(p); + p.addComponentStateParam(this, m_initialized); + Traversal trav = new Traversal () { + protected void act(Component c) { + if (c instanceof Widget) { + ((Widget) c).setValidateInvisible(false); + } + } + }; + + trav.preorder(this); + } + + public void register(Form form, FormModel model) { + super.register(form, model); + m_form = form; + } + + private void addInitialized(String name) { + // m_initialized = new Hidden(new BooleanParameter(name)); + // add(m_initialized); + m_initialized = new BooleanParameter(name); + m_initialized.setDefaultValue(Boolean.FALSE); + } + + public boolean isInitialized(PageState ps) { + // Object init = m_initialized.getValue(ps); + Boolean init = (Boolean)ps.getValue(m_initialized); + if (init == null) { + s_log.debug("init for step " + m_initialized.getName() + " is null. returning true"); + // happens if step state is stored in session - + // form containing this step clears session + // info when processed, but fireProcess invoked + // on this step AFTER form is processed. At that point, + // the step has been initialised because we are on the + // final process at the end of the steps + // + init = Boolean.TRUE; + } + return init.booleanValue(); + } + + private void setInitialized(PageState ps) { + //m_initialized.setValue(ps, Boolean.TRUE); + ps.setValue(m_initialized, Boolean.TRUE); + } + + // Turn off forwarding of init events. + protected FormInitListener createInitListener() { + return new FormInitListener() { + public void init(FormSectionEvent evt) { } + }; + } + + protected void fireSubmitted(FormSectionEvent evt) + throws FormProcessException { + if (isInitialized(evt.getPageState())) { + super.fireSubmitted(evt); + } + } + + protected void fireValidate(FormSectionEvent evt) { + if (isInitialized(evt.getPageState())) { + super.fireValidate(evt); + } + } + + protected void fireProcess(FormSectionEvent evt) + throws FormProcessException { + s_log.debug("fireprocess invoked on Formstep " + m_initialized.getName()); + if (isInitialized(evt.getPageState())) { + super.fireProcess(evt); + + + } + } + + public void generateXML(PageState ps, Element parent) { + if (!isInitialized(ps)) { + FormData fd = m_form.getFormData(ps); + try { + fireInit(new FormSectionEvent(this, ps, fd)); + setInitialized(ps); + } catch (FormProcessException ex) { + s_log.debug("initialization aborted", ex); + fd.addError("Initialization Aborted: " + ex.getMessages()); + } + } + + super.generateXML(ps, parent); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/GridPanel.java b/ccm-core/src/main/java/com/arsdigita/bebop/GridPanel.java new file mode 100755 index 000000000..62e91d764 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/GridPanel.java @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.util.BebopConstants; +import com.arsdigita.bebop.util.PanelConstraints; +import com.arsdigita.bebop.form.Hidden; +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Element; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + *

A container that prints its components in a table. Each child is + * printed in its own table cell. The number of columns can be + * specified in the constructor. The components are put into the table + * in the order in which they were added to the GridPanel + * by filling the table one row + * at a time (filling each row from left to right), from the top of the table + * to the bottom.

+ * + *

The position of the component within the cell can be influenced + * with the following constraints.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Horizontal alignmentUse LEFT, CENTER, or + * RIGHT.
Vertical alignmentUse TOP, MIDDLE, or + * BOTTOM.
Full widthUse FULL_WIDTH to instruct the panel to put + * the component in a row by itself, spanning the full width of the + * table.
Inserting childrenUse INSERT to instruct the panel to + * insert the corresponding component, assuming that it will also be + * laid out by a ColumnPanel with the same number of + * columns.
+ * + * + * + *

Constraints can be combined by + * ORing them together. For example, to print a component in a row of its + * own, left-aligned, at the bottom of its cell, use the constraint + * FULL_WIDTH | LEFT | BOTTOM.

+ * + *

Using the INSERT constraint fuses the current + * GridPanel with the panel of the child to which the + * constraint is applied. For example, consider a {@link Form}, that + * is to have a 2-column format with labels in the left column + * and widgets in the right column. If a {@link FormSection} is added to + * the form, it should be included seamlessly into the parent + * form. To do this, set the INSERT + * constraint when the {@link FormSection} is added to the {@link + * Form}'s GridPanel. At the same time, tell the + * GridPanel used to lay out the {@link FormSection} + * that it is is to be inserted into another panel. + * The following + * pseudo-code illustrates the example. (It assumes that Form and + * FormSection are decorators of the GridPanel.)

+ * + *
+ * Form form = new Form(new GridPanel(2));
+ * FormSection sec = new FormSection(new GridPanel(2, true));
+ * // "true" in the above constructor tells the GridPanel it is inserted.
+ *
+ * sec.add(new Label("Basic Item Metadata"), GridPanel.FULL_WIDTH);
+ * sec.add(new Label("Title:"), GridPanel.RIGHT);
+ * sec.add(new Text("title"));
+ *
+ * form.add(sec, GridPanel.INSERT);
+ * 
+ * + * @see BoxPanel + * @see SplitPanel + * @author David Lutterkort + * @author Stanislav Freidin + * @author Justin Ross + * @version $Id: GridPanel.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class GridPanel extends SimpleContainer + implements BebopConstants, PanelConstraints { + + private static final ChildConstraint DEFAULT_CONSTRAINT + = new ChildConstraint(); + + private int m_numColumns; + + /* + * Explicitly registered constraints for child components. Maps + * Componentss to Constraints + */ + private Map m_childConstraintMap; + + /* + * Is this panel inserted in another one? If so, do not produce + * <table> tags. + */ + private boolean m_isInserted; + + /** + * Creates a table panel with the specified number of columns. + * + * @param numColumns the number of columns in the panel + */ + public GridPanel(int numColumns) { + this(numColumns, false); + } + + /** + * Creates a table panel with the specified number of columns and + * indicates whether the panel is inserted. + * + * @param numColumns the number of columns in the panel + * @param isInserted true if this panel is to be + * printed as a direct child of a GridPanel + * with the same number of columns + * @see #setInserted + */ + public GridPanel(int numColumns, boolean isInserted) { + m_numColumns = numColumns; + setInserted(isInserted); + m_childConstraintMap = new HashMap(); + } + + /** + * Adds a component, specifying constraints. + * @param component the component to add + * @param constraints the constraints for the component + */ + public void add(Component component, int constraints) { + super.add(component); + + m_childConstraintMap.put(component, new ChildConstraint(constraints)); + } + + /** + * Sets whether this panel will be printed inside a + * GridPanel with the same number of columns. If + * inserted is true, no <table> tags will be + * produced to enclose the child components. + * @param true if this panel is to be printed + * inside a GridPanel with the same number of columns + * + */ + public void setInserted(boolean isInserted) { + Assert.isUnlocked(this); + m_isInserted = isInserted; + } + + /** + * Determines whether this panel is to be inserted into another panel. + * @return true if this panel is to be inserted into another panel; + * false otherwise. + * @see #setInserted + */ + public final boolean isInserted() { + return m_isInserted; + } + + /** + * Adds child components as a subtree under table-style nodes. If any of the + * direct children are hidden form widgets, they are added directly to + * parent rather than included in any of the + * cell elements of the panel. + * + *

Generates a DOM fragment: + *

+     * <bebop:gridPanel>
+     *   <bebop:panelRow>
+     *     <bebop:cell> ... cell contents </bebop:cell>
+     *     <bebop:cell> ... cell contents </bebop:cell>
+     *     ...
+     *   </bebop:panelRow>
+     *   <bebop:panelRow>
+     *    <bebop:cell> ... cell contents </bebop:cell>
+     *    <bebop:cell> ... cell contents </bebop:cell>
+     *    ...
+     *   </bebop:panelRow>
+     * </bebop:gridPanel>
+ * + * @param pageState + * @param parent + */ + @Override + public void generateXML(PageState pageState, Element parent) { + if (isVisible(pageState)) { + if (isInserted()) { + generateChildren(pageState, parent); + } else { + Element panel = parent.newChildElement(BEBOP_GRIDPANEL, BEBOP_XML_NS); + exportAttributes(panel); + generateChildren(pageState, panel); + } + } + } + + /* + * Lay out the child components using constraints registered for them, + * generating a DOM tree and extending another. + */ + private void generateChildren(PageState pageState, Element parent) { + int positionInRow = 0; + boolean newRowRequested = true; // First time through we want a new row. + Element row = null; + Element cell = null; + ChildConstraint constraint = null; + + Iterator iter = children(); + while (iter.hasNext()) { + Component child = (Component)iter.next(); + + if (child.isVisible(pageState)) { + if (child instanceof Hidden) { + child.generateXML(pageState, parent); + } else { + constraint = getChildConstraint(child); + + if (constraint.m_isInsert) { + child.generateXML(pageState, parent); + + newRowRequested = true; + } else { + if (positionInRow >= m_numColumns + || constraint.m_isFullWidth + || newRowRequested) { + positionInRow = 0; + + row = parent.newChildElement(BEBOP_PANELROW, BEBOP_XML_NS); + + if (constraint.m_isFullWidth) { + // If the column was full width, we + // want a new row in the next iteration. + newRowRequested = true; + } else if (newRowRequested) { + // Reset to off. + newRowRequested = false; + } + } + + cell = row.newChildElement(BEBOP_CELL, BEBOP_XML_NS); + + child.generateXML(pageState, cell); + + constraint.exportCellAttributes(cell, m_numColumns); + + positionInRow++; + } + } + } + } + } + + /* + * Helper stuff + */ + + private ChildConstraint getChildConstraint(Component component) { + ChildConstraint constraint = + (ChildConstraint)m_childConstraintMap.get(component); + + if (constraint == null) { + constraint = DEFAULT_CONSTRAINT; + } + + return constraint; + } + + private static class ChildConstraint { + public boolean m_isFullWidth; + public boolean m_isInsert; + public String m_horizontalAlignment; + public String m_verticalAlignment; + + public ChildConstraint() { + this(0); + } + + public ChildConstraint(int constraints) { + if ((constraints & LEFT) != 0) { + m_horizontalAlignment = "left"; + } else if ((constraints & CENTER) != 0) { + m_horizontalAlignment = "center"; + } else if ((constraints & RIGHT) != 0) { + m_horizontalAlignment = "right"; + } else { + m_horizontalAlignment = null; + } + + if ((constraints & TOP) != 0) { + m_verticalAlignment = "top"; + } else if ((constraints & MIDDLE) != 0) { + m_verticalAlignment = "middle"; + } else if ((constraints & BOTTOM) != 0) { + m_verticalAlignment = "bottom"; + } else { + m_verticalAlignment = null; + } + + m_isFullWidth = (constraints & FULL_WIDTH) != 0; + + m_isInsert = (constraints & INSERT) != 0; + } + + public void exportCellAttributes(Element cell, int numColumns) { + if (m_horizontalAlignment != null) { + cell.addAttribute("align", m_horizontalAlignment); + } + + if (m_verticalAlignment != null) { + cell.addAttribute("valign", m_verticalAlignment); + } + + if (m_isFullWidth) { + cell.addAttribute("colspan", Integer.toString(numColumns)); + } + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Label.java b/ccm-core/src/main/java/com/arsdigita/bebop/Label.java new file mode 100755 index 000000000..0137aba6d --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/Label.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.event.PrintEvent; +import com.arsdigita.bebop.event.PrintListener; +import com.arsdigita.util.Assert; +import com.arsdigita.globalization.GlobalizedMessage; +import com.arsdigita.xml.Element; + +/** + * A text label displayed to the user for information about and identification + * of certain parts of the screen. Therefore the label has to use a + * GlobalizedMessage for the information presented. + * + * A Label is meant to provide semantically relevant informatin and may not be + * used for fixed arbitrary Text. Use Embedded instead. + * + * (Previous usage: can be used to generate either some static, fixed + * text or a new text string for every request.) + * + * To modify the information with an already locked label use the {@link + * #setLabel(String,PageState)} method which can adjust for each request. + * + * @author David Lutterkort + * @version $Id: Label.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class Label extends DescriptiveComponent implements Cloneable { + + public static final String BOLD = "b"; + public static final String ITALIC = "i"; + + // the default label + private GlobalizedMessage m_label; + // a requestlocal set of labels (to avoid printlisteners) + private final RequestLocal m_requestLabel = new RequestLocal(); + private String m_fontWeight; + + /** The setting for output escaping affects how markup in the + * content is handled. + *
  • If output escaping is in effect (true), <b>example</b> + * will appear literally.
  • + *
  • If output escaping is disabled, <b>example</b> appears as the + * String "example" in bold (i.e. retaining the markup.
+ * Default is false. */ + private boolean m_escaping = false; // default for a primitive anyway + private PrintListener m_printListener; + + /** + * Constructor creates a new Label with empty text. + */ + public Label() { + // A kind of fallback (or a hack) here. Parameter label is taken as + // a key for some (unknown) Resource bundle. Because GlobalizedMessage + // will not find a corrresponding message it will display the key + // itself, 'faking' a globalized message. + m_label = new GlobalizedMessage(" "); + } + + /** + * Creates a new Label with the specified (fixed) text. + * + * @param label the text to display + * @deprecated refactor to use Label(GlobalizedMessage label) instead + */ + public Label(String label) { + this(label, true); + } + + /** + * Creates a new Label with the specified text and + * output escaping turned on if escaping is true. + * + * The setting for output escaping affects how markup in the + * label is handled. For example: + *
  • If output escaping is in effect, <b>text</b> will appear + * literally.
  • + *
  • If output escaping is disabled, <b>text</b> appears as the + * word "text" in bold.
+ * + * @param label the text to display + * @param escaping true if output escaping will be in effect; + * false if output escaping will be disabled + * + * @deprecated refactor to Label(GlobalizedMessage label, boolean escaping) + * instead + */ + public Label(String label, boolean escaping) { + setLabel(label); + setOutputEscaping(escaping); + } + + /** + *

Creates a new label with the specified text.

+ * + * @param label the text to display + */ + public Label(GlobalizedMessage label) { + this(label, true); + } + + /** + * Creates a new label with the specified text as GlobalizedMessage + * and fontweight. + * + * @param label The text to display as GlobalizedMessage + * @param fontWeight The fontWeight e.g., Label.BOLD. Whether it has any + * effect depends on the theme! Take it just as a hint. + */ + public Label(GlobalizedMessage label, String fontWeight) { + this(label, true); + m_fontWeight = fontWeight; + } + + /** + *

Creates a new label with the specified text as GlobalizedMessage + * and output escaping turned on if escaping is + * true.

+ * + * @param label the text to display as GlobalizedMessage + * @param escaping Whether or not to perform output escaping + */ + public Label(GlobalizedMessage label, boolean escaping) { + setLabel(label); + setOutputEscaping(escaping); + } + + /** + * Creates a new Label that uses the print listener to + * generate output. + * + * @param l the print listener used to produce output + */ + public Label(PrintListener l) { + this(); + addPrintListener(l); + } + + /** + * Creates a new label with the specified text and fontweight. + * + * @param label The text to display + * @param fontWeight The fontWeight e.g., Label.BOLD + * + * @deprecated without direct replacement. Refactor to use + * Label(GlobalizedMEssage) instead and modify the theme to + * use proper text marking. (Or use setFontWeight separately. + */ + public Label(String label, String fontWeight) { + this(label, true); + m_fontWeight = fontWeight; + } + + /** + * Provides the Label as Text, localized for the current request. + * + * Although it is not recommended, this method may be overridden to + * dynamically generate the text of the label. Overriding code may need + * the page state. + *

+ * If possible, derived classes should override {@link #getLabel()} instead, + * which is called from this method. As long as we don't have a static + * method to obtain ApplicationContext, this is a way to get the + * RequestContext (to determine the locale). When ApplicationContext gets + * available, that will become the suggested way for overriding code to get + * context. + * + * @param state the current page state + * @return the string produced for this label + */ + public String getLabel(PageState state) { + return (String) getGlobalizedMessage(state).localize(state.getRequest()); + } + + // /** + // * . + // * + // * This method may be overridden to dynamically generate the default text of + // * the label. + // * + // * @return the string produced for this label. + // * + // * @deprecated Use {@link #getGlobalizedMessage()} + // */ + // Conflicts with Super's getLabel message of type GlobalizedMessage. But isn't + // needed anyway. Should deleted as soon as the refactoring of Label is + // completed (i.e. any string Label ironed out). + // public String getLabel() { + // return getGlobalizedMessage().getKey(); + // } + + /** + *

This should really be getLabel(), but since it was marked STABLE I + * can't change its return type.

+ * + * @return the default label to display. + */ + public GlobalizedMessage getGlobalizedMessage() { + return getGlobalizedMessage(null); + } + + /** + *

This should really be getLabel(), but since it was marked STABLE I + * can't change its return type.

+ * + * @param state the current PageState + * @return the label to display for this request, or if state is null, the + * default label + */ + public GlobalizedMessage getGlobalizedMessage(PageState state) { + if (state != null) { + GlobalizedMessage dynlabel = + (GlobalizedMessage) m_requestLabel.get(state); + if (dynlabel != null) { + return dynlabel; + } + } + return m_label; + } + + /** + * Sets new default text for this Label. + * + * @param label The new label text; will be used as a key into the current + * ResourceBundle if possible, or displayed literally. + * @deprecated refactor to use + * @see setLabel(GlobalizedMessage) instead! + */ + public void setLabel(String label) { + setLabel(label, null); + } + + /** + * Sets new request-specific text for this Label to use on this request. If + * state is null, then sets the default text instead. + * + * @param label The new label text; will be used as a key into the current + * ResourceBundle if possible, or displayed literally. + * @param state the page state + * @pre state == null implies !isLocked() + * @deprecated refactor to use + * @see setLabel(GlobalizedMessage, PageState) instead! + */ + public void setLabel(String label, PageState state) { + if (label == null || label.length() == 0) { + label = " "; + } + // A kind of fallback (or a hack) here. Parameter label is taken as + // a key for some (unknown) Resource bundle. Because GlobalizedMessage + // will not find a corrresponding message it will display the key + // itself, 'faking' a globalized message. + setLabel(new GlobalizedMessage(label), state); + } + + /** + * Sets the text for this label using a GlobalizedMessage. + * + * @param label The GlobalizedMessage containing the label text or the + * lookup key to use in the ResourceBundle + * @param state the current page state; if null, sets the default text for + * all requests. + * @pre state == null implies !isLocked() + */ + public void setLabel(GlobalizedMessage label, PageState state) { + if (state == null) { + Assert.isUnlocked(this); + m_label = label; + } else { + m_requestLabel.set(state, label); + } + } + + /** + * Sets the default text for this Label. + * + * Overwrites parent's method an therefore prevents the usage of parent's + * label methods (which are attributes, but here it is the content). + * + * @param label The GlobalizedMessage containing the label text or the + * lookup key to use in the ResourceBundle + */ + @Override + public void setLabel(GlobalizedMessage label) { + setLabel(label, null); + } + + public final boolean getOutputEscaping() { + return m_escaping; + } + + /** + * Controls whether output is escaped during transformation, by default + * true. If true, it will be printed literally, and the user will see + * <b>. When false, the browser will interpret as a bold tag. + * + * @param escaping + */ + public final void setOutputEscaping(boolean escaping) { + m_escaping = escaping; + } + + public final String getFontWeight() { + return m_fontWeight; + } + + public void setFontWeight(String fontWeight) { + Assert.isUnlocked(this); + m_fontWeight = fontWeight; + } + + /** + * Adds a print listener. Only one print listener can be set for a label, + * since the PrintListener is expected to modify the target + * of the PrintEvent. + * + * @param listener the print listener + * @throws IllegalArgumentException if listener is null. + * @throws IllegalStateException if a print listener has previously been + * added. + * @pre listener != null + */ + public void addPrintListener(PrintListener listener) + throws IllegalStateException, IllegalArgumentException { + if (listener == null) { + throw new IllegalArgumentException("Argument listener can not be null"); + } + if (m_printListener != null) { + throw new IllegalStateException("Too many listeners. Can only have one"); + } + m_printListener = listener; + } + + /** + * Removes a previously added print listener. If listener is + * not the listener that was added with {@link #addPrintListener + * addPrintListener}, an IllegalArgumentException will be thrown. + * + * @param listener the listener that was added with + * addPrintListener + * @throws IllegalArgumentException if listener is not the + * currently registered print listener or is null. + * @pre listener != null + */ + public void removePrintListener(PrintListener listener) + throws IllegalArgumentException { + if (listener == null) { + throw new IllegalArgumentException("listener can not be null"); + } + if (listener != m_printListener) { + throw new IllegalArgumentException("listener is not registered with this widget"); + } + m_printListener = null; + } + + /** + * Generates the (J)DOM fragment for a label. + *

+     * <bebop:link href="..." type="..." %bebopAttr;/>
+     * 
+ * + * @param state The current {@link PageState}. + * @param parent The XML element to attach the XML to. + */ + @Override + public void generateXML(PageState state, Element parent) { + + if (!isVisible(state)) { + return; + } + + Label target = firePrintEvent(state); + + Element label = parent.newChildElement("bebop:label", BEBOP_XML_NS); + + target.exportAttributes(label); + target.generateDescriptionXML(state, label); + + String weight = target.getFontWeight(); + if (weight != null && weight.length() > 0) { + label.addAttribute("weight", weight); + } + + if (!target.m_escaping) { + label.addAttribute("escape", "yes"); + } else { + label.addAttribute("escape", "no"); + } + + String key = getGlobalizedMessage() + .getKey() + .substring(getGlobalizedMessage() + .getKey().lastIndexOf(".") + 1); + + // This if clause is needed to prevent printing of keys if the + // GlobalizedMessage was created from a String by this class + if(!key.equals(target.getLabel(state))) { + label.addAttribute("key", key); + } + + /* + * This may break with normal JDOM. We may need to have a node for + * the case where there is no weight. The problem comes in that + * setText *may* kill the other content in the node. It will kill + * the other text, so it may be a good idea anyways. + */ + label.setText(target.getLabel(state)); + } + + /** + * + * @param state + * @return + */ + protected Label firePrintEvent(PageState state) { + Label l = this; + + if (m_printListener != null) { + try { + l = (Label) this.clone(); + m_printListener.prepare(new PrintEvent(this, state, l)); + } catch (CloneNotSupportedException e) { + throw new RuntimeException( + "Couldn't clone Label for PrintListener. " + + "This probably indicates a serious programming error: " + + e.getMessage()); + } + } + + return l; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/List.java b/ccm-core/src/main/java/com/arsdigita/bebop/List.java new file mode 100755 index 000000000..32e74865a --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/List.java @@ -0,0 +1,821 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +import javax.servlet.ServletException; + +import com.arsdigita.bebop.list.ListModel; +import com.arsdigita.bebop.list.ListModelBuilder; +import com.arsdigita.bebop.list.ListCellRenderer; +import com.arsdigita.bebop.list.DefaultListCellRenderer; +import com.arsdigita.bebop.event.ActionEvent; +import com.arsdigita.bebop.event.ActionListener; +import com.arsdigita.bebop.event.ChangeEvent; +import com.arsdigita.bebop.event.ChangeListener; +import com.arsdigita.bebop.event.EventListenerList; +import com.arsdigita.bebop.parameters.StringParameter; +import com.arsdigita.util.Assert; +import com.arsdigita.bebop.util.BebopConstants; + +import com.arsdigita.xml.Element; + + +/** + * A List, similar to a javax.swing.JList, that + * keeps track of a sequence of items and selections of one or more of + * these items. A separate model, {@link ListModel}, is used to represent + * the items in the list. + * + * @see ListModel + * @see ListModelBuilder + * @see com.arsdigita.bebop.list.ListCellRenderer + * @author David Lutterkort + * @version $Id: List.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class List extends SimpleComponent implements BebopConstants { + + /** + * The name of the StringParameter that the list uses to keep track of + * which item is selected. + */ + public static final String SELECTED = "sel"; + + /** + * The name of the event the list sets when producing links that change + * which item is selected. + */ + public static final String SELECT_EVENT = "s"; + + /** + * The model builder for this list. Is used to produce a new model for + * each request served by this List. + * @see #setListModelBuilder + * @see ArrayListModelBuilder + * @see MapListModelBuilder + */ + private ListModelBuilder m_modelBuilder; + + private RequestLocal m_model; + + /** + * The renderer used to format list items. + * @see DefaultListCellRenderer + */ + private ListCellRenderer m_renderer; + + private EventListenerList m_listeners; + + private SingleSelectionModel m_selection; + + private ChangeListener m_changeListener; + + private Component m_emptyView; + + private boolean m_stateParamsAreRegistered; + + + /** + *

Vertical List layout.

+ **/ + public static final int VERTICAL = 0; + + + /** + *

Horizontal List layout.

+ **/ + public static final int HORIZONTAL = 1; + + + private int m_layout = VERTICAL; + + /** + * Creates a new List that uses the specified + * list model builder to generate + * per-request {@link ListModel ListModels}. + * + * @param b the model builder used for this list + * @pre b != null + */ + public List(ListModelBuilder b) { + this(); + m_modelBuilder = b; + m_emptyView = null; + } + + /** + * Creates an empty List. + */ + public List() { + // Force the use of the 'right' constructor + this((SingleSelectionModel) null); + m_selection = + new ParameterSingleSelectionModel(new StringParameter(SELECTED)); + } + + /** + * Create an empty List. + */ + public List(SingleSelectionModel selection) { + // This is the real constructor. All other constructors must call it + // directly or indirectly + super(); + m_renderer = new DefaultListCellRenderer(); + m_listeners = new EventListenerList(); + m_selection = selection; + setListData(new Object[0]); + initListModel(); + m_emptyView = null; + m_stateParamsAreRegistered = true; + } + + /** + * Creates a new List from an array of objects. Uses an + * internal {@link ListModelBuilder}. Each {@link ListModel} that is + * built will + * iterate through the entries of the object, returning the objects from + * calls to {@link ListModel#getElement} and the corresponding index, + * which is converted to a String from calls to {@link + * ListModel#getKey}. + * + * @param values an array of items + * @pre values != null + * @see #setListData(Object[] v) + */ + public List(Object[] values) { + this(); + setListData(values); + } + + /** + * Creates a new List from a map. Uses an internal {@link + * ListModelBuilder}. Each {@link ListModel} that is built will iterate + * through the entries in map in the order in which they are + * returned by map.entrySet().iterator(). Calls to {@link + * ListModel#getElement} return one value in map. Calls to + * {@link ListModel#getElement} return the corresponding key, which is + * converted + * to a String by calling toString() on the key. + * + * @param map a key-value mapping for the list items + * @pre map != null + */ + public List(Map map) { + this(); + setListData(map); + } + + /** + * Registers this List and its state parameter(s) with the + * specified page. + * + * @param p the page this list is contained in + * @pre p != null + * @pre ! isLocked() + */ + public void register(Page p) { + Assert.isUnlocked(this); + if ( m_selection.getStateParameter() != null ) { + p.addComponentStateParam(this, m_selection.getStateParameter()); + } + } + + /** + * Responds to a request in which this List was the targetted + * component. Calls to this method should only be made through links + * generated by this list. + * + *

Determines the new selected element and fires a {@link + * ChangeEvent} if it has changed. After that, fires an {@link + * ActionEvent}. + * + * @param state the state of the current request + * @throws ServletException if the control event is unknown. + * @pre state != null + * @see #fireStateChanged fireStateChanged + * @see #fireActionEvent fireActionEvent + */ + public void respond(PageState state) throws ServletException { + String event = state.getControlEventName(); + + if ( SELECT_EVENT.equals(event) ) { + setSelectedKey(state, state.getControlEventValue()); + } else { + throw new ServletException("Unknown event '" + event + "'"); + } + fireActionEvent(state); + } + + /** + * Generates XML representing the items in the list. The items are + * formatted using a {@link ListCellRenderer}. generateXML + * is called on each component returned by the renderer. + * + *

The XML that is generated has the following form: + *

+     *   <bebop:list mode="single" %bebopAttr;>
+     *     <bebop:cell [selected="selected"] key="itemKey">
+     *        ... XML generated for component returned by renderer ...
+     *     </bebop:cell>
+     *     ... more <bebop:cell> elements, one for each list item ...
+     *   </bebop:list>
+ * + * @param state the state of the current request + * @param parent the element into which XML is generated + * @pre state != null + * @pre parent != null + * @see com.arsdigita.bebop.list.ListCellRenderer + */ + public void generateXML(PageState state, Element parent) { + + if ( ! isVisible(state) ) { + return; + } + + ListModel m = getModel(state); + + // Check if there are items in the list + if(m.next()) { + + // The list has items + + Element list = parent.newChildElement(BEBOP_LIST, BEBOP_XML_NS); + exportAttributes(list); + + if (m_layout == VERTICAL) { + list.addAttribute("layout", "vertical"); + } else { + list.addAttribute("layout", "horizontal"); + } + + Component c; + + Object selKey; + if(getStateParamsAreRegistered()) + { + selKey = getSelectedKey(state); + } + else + selKey = null; + + int i = 0; + do { + Element item = list.newChildElement(BEBOP_CELL, BEBOP_XML_NS); + + String key = m.getKey(); + Assert.exists(key); + + // Converting both keys to String for comparison + // since ListModel.getKey returns a String + boolean selected = (selKey != null) && + key.equals(selKey.toString()); + + item.addAttribute("key", key); + if ( selected ) { + item.addAttribute("selected", "selected"); + } + if(getStateParamsAreRegistered()) + state.setControlEvent(this, SELECT_EVENT, key); + c = getCellRenderer().getComponent(this, state, m.getElement(), + key, i, selected); + c.generateXML(state, item); + i += 1; + } while (m.next()); + + } else { + // The list has no items + if(m_emptyView != null) { + // Display the empty view + m_emptyView.generateXML(state, parent); + } else { + // For compatibility reasons, generate an empty + // list element. In the future, this should go away + Element list = parent.newChildElement(BEBOP_LIST, BEBOP_XML_NS); + exportAttributes(list); + } + } + + state.clearControlEvent(); + } + + + /** + *

Retrieve the current List layout.

+ * + * @return List.VERTICAL or List.HORIZONTAL + **/ + public int getLayout() { + return m_layout; + } + + /** + *

Set the current List layout.

+ * + * @param layout New layout value, must be List.VERTICAL or List.HORIZONTAL + **/ + public void setLayout(int layout) { + Assert.isUnlocked(this); + Assert.isTrue((layout == VERTICAL) || (layout == HORIZONTAL), + "Invalid layout code passed to setLayout"); + m_layout = layout; + } + + /** + * This method is part of a mechanism to freakishly allow + * List's to be used as parent classes for components + * that do not have their state params registered with the + * page. An example of a situation like this is Form ErrorDisplay + * being used in a Metaform + */ + public void setStateParamsAreRegistered(boolean val) + { + m_stateParamsAreRegistered = val; + } + + public boolean getStateParamsAreRegistered() + { + return m_stateParamsAreRegistered; + } + + /** + * Returns the renderer currently used for rendering list items. + * + * @return the current list cell renderer. + * @see #setCellRenderer setCellRenderer + * @see com.arsdigita.bebop.list.ListCellRenderer + */ + public final ListCellRenderer getCellRenderer() { + return m_renderer; + } + + /** + * Sets the cell renderer to be used when generating output with + * or {@link #generateXML generateXML}. + * + * @param r a ListCellRenderer value + * @pre r != null + * @pre ! isLocked() + * @see com.arsdigita.bebop.list.ListCellRenderer + */ + public final void setCellRenderer(ListCellRenderer r) { + Assert.isUnlocked(this); + m_renderer = r; + } + + /** + * Returns the model builder currently used to build each request-specific + * {@link ListModel}. + * + * @return a ListModelBuilder value. + * @see #setModelBuilder setModelBuilder + * @see ListModelBuilder + */ + public final ListModelBuilder getModelBuilder() { + return m_modelBuilder; + } + + /** + * Sets the model builder used to build each request-specific + * {@link ListModel}. + * + * @param b a ListModelBuilder value + * @pre ! isLocked() + * @see ListModelBuilder + */ + public final void setModelBuilder(ListModelBuilder b) { + Assert.isUnlocked(this); + m_modelBuilder = b; + } + + /** + * Sets the empty view component, which is + * shown if there are no items in the list. This component must + * be stateless. For example, it could be an Image or a Label. + * + * @param c the new empty view component + */ + public final void setEmptyView(Component c) { + Assert.isUnlocked(this); + m_emptyView = c; + } + + /** + * Gets the empty view component. The empty view component is + * shown if there are no items in the list. + * @return the empty view component. + */ + public final Component getEmptyView() { + return m_emptyView; + } + + + /** + * Initialize the private m_model variable. The initial + * value is what the model builder returns for the state. + */ + private void initListModel() { + m_model = new RequestLocal() { + protected Object initialValue(PageState s) { + return getModelBuilder().makeModel(List.this, s); + } + }; + } + + /** + * Gets the list model used in processing the request represented by + * state. + * + * @param state the state of the current request + * @return the list model used in processing the request represented by + * state. + */ + public ListModel getModel(PageState state) { + return (ListModel) m_model.get(state); + } + + /** + * Sets the list to use for the values in values. Each {@link + * ListModel} that is built will iterate through the entries of the object, + * returning the objects from calls to {@link ListModel#getElement} and + * the corresponding index, which is converted to a String + * from calls to {@link ListModel#getKey}. + * + * @param values an array of items + * @pre values != null + * @pre ! isLocked() + */ + public void setListData(Object[] values) { + Assert.isUnlocked(this); + m_modelBuilder = new ArrayListModelBuilder(values); + } + + /** + * Sets the list to use the entries in map. Each {@link + * ListModel} that is built will iterate through the entries in + * map in the order in which they are returned by + * map.entrySet().iterator(). Calls to {@link + * ListModel#getElement} return one value in map. Calls to + * {@link ListModel#getElement} return the corresponding key, which is + * converted to a String by calling toString() + * on the key. + * @param map a key-value mapping for the list items + * @pre map != null + * @pre ! isLocked() + */ + public void setListData(Map map) { + Assert.isUnlocked(this); + m_modelBuilder = new MapListModelBuilder(map); + } + + /** + * Gets the selection model. The model keeps track of which list item is + * currently selected, and can be used to manipulate the selection + * programmatically. + * + * @return the model used by the list to keep track of the selected list + * item. + */ + public final SingleSelectionModel getSelectionModel() { + return m_selection; + } + + /** + * Sets the selection model that the list uses to keep track of the + * currently selected list item. + * + * @param m the new selection model + * @pre m != null + * @pre ! isLocked() + */ + public final void setSelectionModel(SingleSelectionModel m) { + Assert.isUnlocked(this); + if ( m_changeListener != null ) { + // Transfer the change listener + m_selection.removeChangeListener(m_changeListener); + m.addChangeListener(m_changeListener); + } + m_selection = m; + } + + /** + * Gets the key for the selected list item. This will only + * be a valid key + * if {@link #isSelected isSelected} is true. + * + * @param state the state of the current request + * @return the key for the selected list item. + * @pre isSelected(state) + */ + public Object getSelectedKey(PageState state) { + return m_selection.getSelectedKey(state); + } + + /** + * Sets the selection to the one with the specified key. If + * key was not already selected, fires the {@link + * ChangeEvent}. + * + * @param state the state of the current request + * @param key the key for the selected list item + * @see #fireStateChanged fireStateChanged + */ + public void setSelectedKey(PageState state, String key) { + m_selection.setSelectedKey(state, key); + } + + /** + * Returns true if one of the list items is currently selected. + * + * @param state the state of the current request + * @return true if one of the list items is selected + * false otherwise. + */ + public boolean isSelected(PageState state) { + return m_selection.isSelected(state); + } + + /** + * Clears the selection in the request represented by state. + * + * @param state the state of the current request + * @post ! isSelected(state) + */ + public void clearSelection(PageState state) { + m_selection.clearSelection(state); + } + + /** + * Creates the change listener that is used for forwarding change events + * fired by + * the selection model to change listeners registered with the list. The + * returned change listener refires the event with the list, + * rather than the selection model, as source. + * + * @return the change listener used internally by the list. + */ + protected ChangeListener createChangeListener() { + return new ChangeListener() { + public void stateChanged(ChangeEvent e) { + fireStateChanged(e.getPageState()); + } + }; + } + + /** + * Adds a change listener. A change event is fired whenever the selected + * list item changes during the processing of a request. The change event + * that listeners receive names the list as the source. + * + * @param l the change listener to run when the selected item changes in + * a request + * @pre ! isLocked() + */ + public void addChangeListener(ChangeListener l) { + Assert.isUnlocked(this); + if ( m_changeListener == null ) { + m_changeListener = createChangeListener(); + m_selection.addChangeListener(m_changeListener); + } + m_listeners.add(ChangeListener.class, l); + } + + /** + * Removes a change listener. The listener should have been previously + * added with {@link #addChangeListener addChangeListener}, although no + * error is signalled if the change listener is not found among the + * list's listeners. + * + * @param l the change listener to remove from the list + */ + public void removeChangeListener(ChangeListener l) { + Assert.isUnlocked(this); + m_listeners.remove(ChangeListener.class, l); + } + + /** + * Fires a change event to signal that the selected list item has changed + * in the request represented by state. The source of the + * event is the list. + * + * @param state the state of the current request + */ + protected void fireStateChanged(PageState state) { + Iterator + i=m_listeners.getListenerIterator(ChangeListener.class); + ChangeEvent e = null; + + while (i.hasNext()) { + if ( e == null ) { + e = new ChangeEvent(this, state); + } + ((ChangeListener) i.next()).stateChanged(e); + } + } + + // Action events + /** + * Adds an action listener. This method is run whenever {@link #respond respond} is + * called on the list. This gives clients a way to track mouse clicks + * received by the list. + * @param 1 the action listener to add + * + * @pre l != null + * @pre ! isLocked() + * @see #respond respond + */ + public void addActionListener(ActionListener l) { + Assert.isUnlocked(this); + m_listeners.add(ActionListener.class, l); + } + + /** + * Removes a previously added action listener. + * @param 1 the action listener to remove + * + * @see #addActionListener addActionListener + */ + public void removeActionListener(ActionListener l) { + Assert.isUnlocked(this); + m_listeners.remove(ActionListener.class, l); + } + + /** + * Fires an action event signalling that the list received the request + * submission. All registered action listeners are run. The source of + * the event is the list. + * @param state the state of the current request + * + * @pre state != null + * @see #respond respond + */ + protected void fireActionEvent(PageState state) { + Iterator + i=m_listeners.getListenerIterator(ActionListener.class); + ActionEvent e = null; + + while (i.hasNext()) { + if ( e == null ) { + e = new ActionEvent(this, state); + } + ((ActionListener) i.next()).actionPerformed(e); + } + } + + // ListModelBuilder for maps + + /** + * Build list models from a map. The list models use the result of + * toString() called on the key of the map entries as their + * keys and return the associated value as the element for the list items + * the list model iterates over. + */ + private static class MapListModelBuilder implements ListModelBuilder { + private Map m_map; + private boolean m_locked; + + public MapListModelBuilder() { + this(Collections.EMPTY_MAP); + } + + public MapListModelBuilder(Map m) { + m_map = m; + } + + public ListModel makeModel(List l, PageState state) { + return new ListModel() { + private Iterator i = m_map.entrySet().iterator(); + private Map.Entry e = null; + + public boolean next() { + if ( ! i.hasNext() ) { + e = null; + return false; + } + e = (Map.Entry) i.next(); + return true; + } + + public Object getElement() { + checkState(); + return e.getValue(); + } + + public String getKey() { + checkState(); + return e.getKey().toString(); + } + + private void checkState() { + if ( e == null ) { + throw new IllegalStateException("No valid current item. " + + "Model is either before first item or after last item"); + } + } + + }; + } + + public void lock() { + m_locked = true; + } + + public final boolean isLocked() { + return m_locked; + } + } + + // ListModelBuilder for arrays + + /** + * Build list models from an array of values. The list models use the + * index of the array entries, converted to a String, as the + * key for the list items and the array values as their elements. + */ + private static class ArrayListModelBuilder implements ListModelBuilder { + private Object[] m_values; + private boolean m_locked; + + public ArrayListModelBuilder() { + this(new Object[0]); + } + + public ArrayListModelBuilder(Object[] values) { + m_values = values; + } + + public ListModel makeModel(List l, PageState state) { + return new ListModel() { + private int i = -1; + + public boolean next() { + i += 1; + return ( i < m_values.length ); + } + + public Object getElement() { + checkState(); + return m_values[i]; + } + + public String getKey() { + checkState(); + return String.valueOf(i); + } + + private void checkState() { + if ( i < 0 ) { + throw new IllegalStateException + ("Before first item. Call next() first."); + } + if ( i >= m_values.length ) { + throw new IllegalStateException("After last item. Model exhausted."); + } + } + }; + } + + public void lock() { + m_locked = true; + } + + public final boolean isLocked() { + return m_locked; + } + } + + /** + * A {@link ListModel} that has no rows. + */ + public static final ListModel EMPTY_MODEL = new ListModel() { + public boolean next() { + return false; + } + + public String getKey() { + throw new IllegalStateException("ListModel is empty"); + } + + public Object getElement() { + throw new IllegalStateException("ListModel is empty"); + } + }; +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Page.java b/ccm-core/src/main/java/com/arsdigita/bebop/Page.java new file mode 100755 index 000000000..6ec4cc0de --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/Page.java @@ -0,0 +1,1344 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.event.ActionEvent; +import com.arsdigita.bebop.event.ActionListener; +import com.arsdigita.bebop.event.RequestEvent; +import com.arsdigita.bebop.event.RequestListener; +import com.arsdigita.bebop.parameters.BitSetParameter; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.bebop.parameters.StringParameter; +import com.arsdigita.bebop.util.Traversal; +import com.arsdigita.kernel.KernelConfig; +import com.arsdigita.util.Assert; +import com.arsdigita.util.SystemInformation; +import com.arsdigita.xml.Document; +import com.arsdigita.xml.Element; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.log4j.Logger; + +/** + * The top-level container for all Bebop components and containers. + * + *
    + *
  • Holds references to the components of a page.
  • + *
  • Provides methods for servicing requests and for notifying other + * components that a request for this page has been received through + * {@link ActionListener ActionListeners}.
  • + *
  • Tracks request parameters for stateful components, such as tabbed panes + * and sortable tables.
  • + *
+ * + * A typical Page may be created as follows: null

+ * Page p = new Page("Hello World");
+ * p.add(new Label("Hello World");
+ * p.lock();
+ * 

+ * + * @author David Lutterkort + * @author Stanislav Freidin + * @author Uday Mathur + * + * @version $Id: Page.java 1270 2006-07-18 13:34:55Z cgyg9330 $ + */ +public class Page extends SimpleComponent implements Container { + + /** + * Class specific logger instance. + */ + private static final Logger s_log = Logger.getLogger(Page.class); + /** + * The delimiter character for components naming + */ + private static final String DELIMITER = "."; + /** + * The prefix that gets prepended to all state variables. Components must + * not use variables starting with this prefix. This guarantees that the + * page state and variables individual components wish to pass do not + * interfere with each other. + */ + private static final String COMPONENT_PREFIX = "bbp" + DELIMITER; + private static final String INTERNAL = COMPONENT_PREFIX; + /** + * The name of the special parameter that indicates which component has been + * selected. + */ + static final String SELECTED = INTERNAL + "s"; + static final String CONTROL_EVENT = INTERNAL + "e"; + static final String CONTROL_VALUE = INTERNAL + "v"; + static final Collection CONTROL_EVENT_KEYS; + + static { + s_log.debug("Static initalizer is starting..."); + CONTROL_EVENT_KEYS = new ArrayList(3); + CONTROL_EVENT_KEYS.add(SELECTED); + CONTROL_EVENT_KEYS.add(CONTROL_EVENT); + CONTROL_EVENT_KEYS.add(CONTROL_VALUE); + s_log.debug("Static initalizer finished."); + } + + /** + * The name of the request parameter used for the visibility state of + * components stored in m_invisible. + */ + static final String INVISIBLE = INTERNAL + "i"; + /** + * Map of stateful components (id --> Component) SortedMap used because + * component based hash for page is based on concatenation of component ids, + * and so need to guarantee that they are returned in the same order for the + * same page - cg. + */ + private SortedMap m_componentMap; + private List m_components; + /** + * Map of component -> owned parameter collection + */ + private Map m_componentParameterMap = new HashMap(); + private FormModel m_stateModel; + /** + * Container that renders this Page. + */ + protected Container m_panel; + private List m_actionListeners; + private List m_requestListeners; + /** + * The title of the page to be added in the head of HTML output. The title + * is wrapped in a Label to allow developers to add PrintListeners to + * dynamically change the value of the title. + */ + private Label m_title; + /** + * Stores the actual title for the current request. The title may be + * generated with a PrintListener of the m_title Label. + */ + private RequestLocal m_currentTitle; + /** + * A list of all the client-side stylesheets. The elements of the list are + * of type Page.Stylesheet, defined at the end of this file. + */ + private List m_clientStylesheets; + private StringParameter m_selected; + private StringParameter m_controlEvent; + private StringParameter m_controlValue; + /** + * The default (initial) visibility of components. The encoding is identical + * to that for PageState.m_invisible. + * + * This variable is package-friendly since it needs to be accessed by + * PageState. + */ + protected BitSet m_invisible; + /** + * The PageErrorDisplay component that will display page state validation + * errors on this page + */ + private Component m_errorDisplay; + /** + * Indicates whether finish() has been called on this Page. + */ + private boolean m_finished = false; + /** + * indicates whether pageState.stateAsURL() should export the entire state + * for this page, or whether it should only export the control event as a + * URL and use the HttpSession for the rest of the page state. + */ + private boolean m_useHttpSession = false; + + /** + * Returns true if this page should export state through the + * HttpSession instead of the URL query string. + *

+ * If this returns true, then PageState.stateAsURL() will only + * export the control event as a URL query string. If this returns + * false, then stateAsURL() will export the entire page state. + * + * @see PageState#stateAsURL + * + * @return true if this page should export state through the + * HttpSession; false if it should export using the URL + * query string. + */ + public boolean isUsingHttpSession() { + return m_useHttpSession; + } + + /** + * Indicates to this page whether it should export its entire state to + * subsequent requests through the URL query string, or if it should use the + * HttpSession instead and only use the URL query string for the control + * event. + * + * @see PageState#stateAsURL + * + * @param b true if PageState.stateAsURL() will export only the + * control event as a URL query string. false if + * stateAsURL() will export the entire page state. + */ + public void setUsingHttpSession(boolean b) { + m_useHttpSession = b; + } + + // //////////////////////////////////////////////////////////////////////// + // Constructor Section + // //////////////////////////////////////////////////////////////////////// + /** + * Constructor, creates an empty page with the specified title and panel. + * + * @param title title for this page + * @param panel container for this page + * + * @deprecated use Page(Lab el, Container) instead. + */ + public Page(String title, Container panel) { + this(new Label(title), panel); + } + + /** + * Constructor, creates an empty page with the specified title and panel. + * + * @param title title for this page as (globalized) Label + * @param panel container for this page + */ + public Page(Label title, Container panel) { + super(); + m_actionListeners = new LinkedList(); + m_requestListeners = new LinkedList(); + m_panel = panel; + m_clientStylesheets = new ArrayList(); + m_components = new ArrayList(); + m_componentMap = new TreeMap(); + setErrorDisplay(new PageErrorDisplay()); + m_title = title; + + // Initialize the RequestLocal where the title for the current + // request will be kept + m_currentTitle = new RequestLocal() { + + @Override + protected Object initialValue(PageState state) { + return m_title.firePrintEvent(state); + } + + }; + + // Initialize the set of state parameters to hold + // the ones necessary for keeping track of the selected component and + // the name and value of a 'control event' + m_selected = new StringParameter(SELECTED); + m_controlEvent = new StringParameter(CONTROL_EVENT); + m_controlValue = new StringParameter(CONTROL_VALUE); + + m_stateModel = new FormModel("stateModel", true); + m_stateModel.addFormParam(m_selected); + m_stateModel.addFormParam(m_controlEvent); + m_stateModel.addFormParam(m_controlValue); + + // Set up the visibility tracking parameters + m_invisible = new BitSet(32); + BitSetParameter p = new BitSetParameter(INVISIBLE, + BitSetParameter.ENCODE_DGAP); + m_stateModel.addFormParam(p); + } + + /** + * Creates an empty page with default title and implicit BoxPanel container. + */ + public Page() { + this(""); + } + + /** + * Creates an empty page with the specified title and implicit BoxPanel + * container. + * + * @param title title for this page + */ + public Page(Label title) { + this(title, new BoxPanel()); + BoxPanel bp = (BoxPanel) m_panel; + bp.setWidth("100%"); + } + + /** + * Creates an empty page with the specified title and implicit BoxPanel + * container. + * + * @param title title for this page + */ + public Page(String title) { + this(new Label(title)); + } + + /** + * Adds a component to this container. + * + * @param c component to add to this container + */ + @Override + public void add(Component c) { + m_panel.add(c); + + } + + /** + * Adds a component with the specified layout constraints to this container. + * Layout constraints are defined in each layout container as static ints. + * To specify multiple constraints, use bitwise OR. + * + * @param c component to add to this container + * @param constraints layout constraints (a bitwise OR of static ints in the + * particular layout) + */ + @Override + public void add(Component c, int constraints) { + m_panel.add(c, constraints); + } + + /** + * Returns true if this list contains the specified element. + * More formally, returns true if and only if this list + * contains at least one element e such that (o==null ? e==null : + * o.equals(e)). + *

+ * This method returns true only if the component has been + * directly added to this container. If this container contains another + * container that contains this component, this method returns + * false. + * + * @param o element whose presence in this container is to be tested + * + * @return true if this Container contains the specified + * component directly; false otherwise. + */ + @Override + public boolean contains(Object o) { + return m_panel.contains(o); + } + + /** + * Returns the component at the specified position. Each call to the add + * method increments the index. Since the user has no control over the index + * of added components (other than counting each call to add), this method + * should be used in conjunction with indexOf. + * + * @param index the index of the item to be retrieved from this Container + * + * @return the component at the specified position in this container. + */ + @Override + public Component get(int index) { + return m_panel.get(index); + } + + /** + * Gets the index of a component. + * + * @param c component to search for + * + * @return the index in this list of the first occurrence of the specified + * element, or -1 if this list does not contain this element. + * + * @pre c != null + * @post contains(c) implies (return >= 0) && (return < size()) @pos t + * !contains(c) implies return == -1 + */ + @Override + public int indexOf(Component c) { + return m_panel.indexOf(c); + } + + /** + * Returns true if the container contains no components. + * + * @return true if this container contains no components; + * false otherwise. + */ + @Override + public boolean isEmpty() { + return m_panel.isEmpty(); + } + + /** + * Returns the number of elements in this container. This does not + * recursively count the components that are indirectly contained in this + * container. + * + * @return the number of components directly in this container. + */ + @Override + public int size() { + return m_panel.size(); + } + + @Override + public Iterator children() { + return Collections.singletonList(m_panel).iterator(); + } + + /** + * Returns the panel that the Page uses for rendering its + * components. + * + * @return the panel. + */ + public final Container getPanel() { + return m_panel; + } + + /** + * Set the Container used for rendering components on this page. Caution + * should be used with this function, as the existing container is simply + * overwritten. + * + * @param c + * + * @author Matthew Booth (mbooth@redhat.com) + */ + public void setPanel(Container c) { + m_panel = c; + } + + /** + * Retrieves the title of this page. + * + * @return the static title of this page. + */ + public final Label getTitle() { + return m_title; + } + + /** + * Retrieves the title of this page as a Bebop label component. + * + * @param state the state of the current request + * + * @return the title of the page for the current request. + */ + public final Label getTitle(PageState state) { + return (Label) m_currentTitle.get(state); + } + + /** + * Sets the title for this page from the passed in string. + * + * @param title title for this page + */ + public void setTitle(String title) { + Assert.isUnlocked(this); + setTitle(new Label(title)); + } + + /** + * Set the title for this page from the passed in label. + * + * @param title title for this page + */ + public void setTitle(Label title) { + Assert.isUnlocked(this); + m_title = title; + } + + /** + * Sets the {@link Component} that will display the validation errors in the + * current {@link PageState}. Any validation error in the + * PageState will cause the Page to completely + * ignore all other components and render only the error display component. + *

+ * By default, a {@link PageErrorDisplay} component is used to display the + * validation errors. + * + * @param c the component that will display the validation errors in the + * current PageState + */ + public final void setErrorDisplay(Component c) { + Assert.isUnlocked(this); + m_errorDisplay = c; + } + + /** + * Gets the {@link Component} that will display the validation errors in the + * current {@link PageState}. Any validation error in the + * PageState will cause the Page to completely + * ignore all other components and render only the error display component. + *

+ * By default, a {@link PageErrorDisplay} component is used to display the + * validation errors. + * + * @return the component that will display the validation errors in the + * current PageState. + */ + public final Component getErrorDisplay() { + return m_errorDisplay; + } + + /** + * Adds a client-side stylesheet that should be used in HTML output. + * Arbitrarily many client-side stylesheets can be added with this method. + * To use a CSS stylesheet, call something like + * setStyleSheet("style.css", "text/css"). + * + *

+ * These values will ultimately wind up in a <link> + * tag in the head of the HTML page. + * + *

+ * Note that the stylesheet set with this call has nothing to do with the + * XSLT stylesheet (transformer) that is applied to the XML generated from + * this page! + * + * @param styleSheetURI the location of the stylesheet + * @param mimeType the MIME type of the stylesheet, usually + * text/css + * + * @pre ! isLocked() + */ + public void addClientStylesheet(String styleSheetURI, String mimeType) { + m_clientStylesheets.add(new Stylesheet(styleSheetURI, mimeType)); + } + + /** + * Adds a global state parameter to this page. Global parameters are values + * that need to be preserved between requests, but that have no special + * connection to any of the components on the page. For a page that displays + * details about an item, a global parameter would be used to identify the + * item. + * + * If the parameter was previously added as a component state parameter, its + * name is unmangled and stays unmangled. + * + * @see #addComponentStateParam + * + * @param p the global parameter to add + * + * @pre ! isLocked() + * @pre parameter != null + */ + public void addGlobalStateParam(ParameterModel p) { + Assert.isUnlocked(this); + p.setName(unmangle(p.getName())); + m_stateModel.addFormParam(p); + } + + /** + * Constructs the top nodes of the DOM or JDOM tree. Used by + * generateXML(PageState, Document) below. + *

+ * Generates DOM fragment: + *

+     * <bebop:page>
+     *   <bebop:title> ... value set with setTitle ... </bebop:title>
+     *   <bebop:stylesheet href='styleSheetURI' type='mimeType'>
+     *   ... page content gnerated by children ...
+     * </bebop:page>
The content of the <title> + * element can be set by calling {@link #setTitle setTitle}. The + * <stylesheet> element will only be present if a stylesheet + * has been set with {@link + * #setStyleSheet setStyleSheet}. + * + * @param ps the page state for the current page + * @param parent the DOM node for the whole Document + * + * @return + * + * @pre isLocked() + */ + protected Element generateXMLHelper(PageState ps, Document parent) { + Assert.isLocked(this); + + Element page = parent.createRootElement("bebop:page", BEBOP_XML_NS); + exportAttributes(page); + + /* Generator information */ + exportSystemInformation(page); + + Element title = page.newChildElement("bebop:title", BEBOP_XML_NS); + title.setText(getTitle(ps).getLabel(ps)); + + for (Iterator i = m_clientStylesheets.iterator(); i.hasNext();) { + ((Stylesheet) i.next()).generateXML(page); + } + + return page; + } + + /** + * Constructs a DOM or JDOM tree with all components on the page. The tree + * represents the page that results from the + * {@link javax.servlet.http.HttpServletRequest} kept in the + * state. + * + * @param state the page state produced by {@link #process} + * @param parent the DOM node for the whole Document + * + * @see #process process + * @pre isLocked() + * @pre state != null + */ + public void generateXML(PageState state, Document parent) { + // always export page state as HTTP session + if (m_useHttpSession) { + state.stateAsHttpSession(); + } + + Element page = generateXMLHelper(state, parent); + + // If the page state has errors, ignore all the components and + // render only the error display component + if (state.getErrors().hasNext()) { + m_errorDisplay.generateXML(state, page); + } else { + m_panel.generateXML(state, page); + } + + if (KernelConfig.getConfig().isDebugEnabled() + && debugStructure(state.getRequest())) { + + Element structure = page.newChildElement("bebop:structure", + BEBOP_XML_NS); + + showStructure(state, structure); + } + } + + private static boolean debugStructure(HttpServletRequest req) { + return "transform".equals(req.getParameter("debug")); + } + + /** + * Do nothing. Top-level add nodes is meaningless. + * + * @param elt + */ + @Override + public void generateXML(PageState state, Element elt) { + } + + /** + * Creates a PageState object and processes it by calling the respond method + * on the selected component. Processes a request by notifying the component + * from which the process originated and {@link #fireActionEvent + * broadcasts} an {@link ActionEvent} to all the listeners that registered + * with {@link #addActionListener addActionListener}. + * + * @see #generateXML(PageState,Document) generateXML + * + * @param request + * @param response + * + * @return + * + * @throws javax.servlet.ServletException + * @pre isLocked() + * @pre request != null + * @pre response != null + */ + public PageState process(HttpServletRequest request, + HttpServletResponse response) + throws ServletException { + + PageState result = new PageState(this, request, response); + try { + process(result); + } finally { + } + return result; + } + + /** + * Processes the supplied PageState object according to this PageModel. + * Calls the respond method on the selected Bebop component. + */ + public void process(PageState state) throws ServletException { + Assert.isLocked(this); + try { + fireRequestEvent(state); + } finally { + } + + // Validate the state; any errors in the state will be displayed + // by generateXML + state.forceValidate(); + + if (state.isValid()) { + try { + state.respond(); + } finally { + } + try { + fireActionEvent(state); + } finally { + + } + } + } + + /** + * Builds a DOM Document from the current request state by doing a + * depth-first tree walk on the current set of components in this Page, + * calling generateXML on each. Does NOT do the rendering. If the HTTP + * response has already been committed, does not build the XML document. + * + * @return a DOM ready for rendering, or null if the response has already + * been committed. + * + * @post res.isCommitted() == (return == null) + */ + public Document buildDocument(HttpServletRequest req, + HttpServletResponse res) + throws ServletException { + try { + Document doc = new Document(); + PageState state = process(req, res); + + // only generate XML document if the response is not already + // committed + if (!res.isCommitted()) { + try { + generateXML(state, doc); + } finally { + } + return doc; + } else { + return null; + } + } catch (ParserConfigurationException e) { + throw new ServletException(e); + } + } + + /** + * Finishes building the page. The tree of components is traversed and each + * component is told to add its state parameters to the page's state model. + * + * @pre ! isLocked() + */ + private void finish() { + if (!m_finished) { + Assert.isUnlocked(this); + + Traversal componentRegistrar = new Traversal() { + + @Override + protected void act(Component c) { + addComponent(c); + c.register(Page.this); + } + + }; + if (m_panel == null) { + s_log.warn("m_panel is null"); + } + componentRegistrar.preorder(m_panel); + if (m_errorDisplay != null) { + addComponent(m_errorDisplay); + m_errorDisplay.register(Page.this); + } + + m_finished = true; + } + } + + /** + * Locks the page and all its components against further modifications. + * + *

+ * Locking a page helps in finding mistakes that result from modifying a + * page's structure.

+ */ + @Override + public void lock() { + if (!m_finished) { + finish(); + } + m_stateModel.lock(); + Traversal componentLocker = new Traversal() { + + @Override + protected void act(Component c) { + c.lock(); + } + + }; + + componentLocker.preorder(m_panel); + + super.lock(); + } + + @Override + public void respond(PageState state) throws javax.servlet.ServletException { + throw new UnsupportedOperationException(); + } + + /** + * Registers a listener that is notified whenever a request to this page is + * made, after the selected component has had a chance to respond. + * + * @pre l != null + * @pre ! isLocked() + */ + public void addActionListener(ActionListener l) { + Assert.isUnlocked(this); + m_actionListeners.add(l); + } + + /** + * Remove a previously registered action listener. + * + * @pre l != null + * @pre ! isLocked() + */ + public void removeActionListener(ActionListener l) { + Assert.isUnlocked(this); + m_actionListeners.remove(l); + } + + /** + * Registers a listener that is notified whenever a request to this page is + * made, before the selected component has had a chance to respond. + * + * @pre l != null + * @pre ! isLocked() + */ + public void addRequestListener(RequestListener l) { + Assert.isUnlocked(this); + m_requestListeners.add(l); + } + + /** + * Removes a previously registered request listener. + * + * @param 1 the listener to remove + * + * @pre l != null + * @pre ! isLocked() + */ + public void removeRequestListener(RequestListener l) { + Assert.isUnlocked(this); + m_requestListeners.remove(l); + } + + /** + * Broadcasts an {@link ActionEvent} to all registered listeners. The source + * of the event is this page, and the state recorded in the event is the one + * resulting from processing the current request. + * + * @param the state for this event + * + * @pre state != null + */ + protected void fireActionEvent(PageState state) { + ActionEvent e = null; + + for (Iterator i = m_actionListeners.iterator(); i.hasNext();) { + if (e == null) { + e = new ActionEvent(this, state); + } + + final ActionListener listener = (ActionListener) i.next(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Firing action listener " + listener); + } + + listener.actionPerformed(e); + } + } + + /** + * Broadcasts a {@link RequestEvent} to all registered listeners. The source + * of the event is this page, and the state recorded in the event is the one + * resulting from processing the current request. + * + * @param state the state for this event + * + * @pre state != null + */ + protected void fireRequestEvent(PageState state) { + RequestEvent e = null; + + for (Iterator i = m_requestListeners.iterator(); i.hasNext();) { + if (e == null) { + e = new RequestEvent(this, state); + } + + final RequestListener listener = (RequestListener) i.next(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Firing request listener " + listener); + } + + listener.pageRequested(e); + } + } + + /** + * Export page generator information if set. The m_pageGenerator is a + * HashMap containing the information as key value. In general this should + * include generator name and generator version. + * + * @param page parent element - should be bebeop:page + * + * @pre m_pageGenerator != null && !m_pageGenerator.isEmpty() + */ + final protected void exportSystemInformation(Element page) { + SystemInformation sysInfo = SystemInformation.getInstance(); + if (!sysInfo.isEmpty()) { + Element gen = page.newChildElement("bebop:systemInformation", + BEBOP_XML_NS); + + Iterator> keyValues = sysInfo.iterator(); + while (keyValues.hasNext()) { + Map.Entry entry = keyValues.next(); + gen.addAttribute(entry.getKey(), entry.getValue()); + } + } + } + + // Client-side stylesheet storage + private class Stylesheet { + + String m_URI; + String m_type; + + public Stylesheet(String stylesheetURI, String mimeType) { + m_URI = stylesheetURI; + m_type = mimeType; + } + + public void generateXML(Element parent) { + Element style = parent.newChildElement("bebop:stylesheet", + BEBOP_XML_NS); + style.addAttribute("href", m_URI); + if (m_type != null) { + style.addAttribute("type", m_type); + } + } + + } + + /** + * Adds a component to the page model. + * + * @deprecated This method will become private in ACS 5.0. + */ + public void addComponent(Component c) { + Assert.isUnlocked(this); + + if (!stateContains(c)) { + if (c == null) { + s_log.error("c is null"); + } /*else { + s_log.error("c: " + c.toString()); + }*/ + + String key = c.getKey(); + if (key == null) { + key = Integer.toString(m_components.size()); + } + if (m_componentMap.get(key) != null) { + throw new IllegalArgumentException( + "Component key must not be duplicated. The key " + key + + " is shared by more than one component."); + } + m_componentMap.put(key, c); + m_components.add(c); + } + } + + /** + * Registers a state parameter for a component. It is permissible to + * register the same state parameter several times, from the same or + * different components. The name of the parameter will be changed to ensure + * that it won't clash with any other component's parameter. If the + * parameter is added more than once, the name is only changed the first + * time it is added. + * + * @param c the component to register the parameter for + * @param p the state parameter to register + * + * @see #addGlobalStateParam + * + * @pre stateContains(c) + * @pre ! isLocked() + * @pre p != null + */ + public void addComponentStateParam(Component c, ParameterModel p) { + Assert.isUnlocked(this); + + if (!stateContains(c)) { + throw new IllegalArgumentException( + "Component must be registered in Page"); + } + if (!m_stateModel.containsFormParam(p)) { + String name = parameterName(c, p.getName()); + s_log.debug(String + .format("Setting name of parameter to add to '%s'", + name)); + p.setName(name); + m_stateModel.addFormParam(p); + + Collection params = (Collection) m_componentParameterMap.get(c); + if (params == null) { + params = new ArrayList(); + m_componentParameterMap.put(c, params); + } + params.add(p); + } + } + + /** + *

+ * Get the parameters registered for a given component.

+ */ + public Collection getComponentParameters(Component c) { + return (Collection) m_componentParameterMap.get(c); + } + + /** + * Gets the state index of a component. This is the number assigned to the + * component in the register traveral + * + * @param c the component to search for + * + * @return the index in this list of the first occurrence of the specified + * element, or -1 if this list does not contain this element. + * + * @pre c != null + * @post contains(c) implies (return >= 0) && (return < size()) @pos t + * !contains(c) implies return == -1 + */ + public int stateIndex(Component c) { + return m_components.indexOf(c); + } + + /** + * The number of components in the page model. + * + * @post return >= 0 + */ + public int stateSize() { + return m_components.size(); + } + + /** + * Checks whether this component is already in the page model. + * + * @pre c != null + */ + public boolean stateContains(Component c) { + return m_components.contains(c); + } + + /** + * Gets a page component by index. + * + * @pre (i >= 0) && (i < size()) @pos t return != null + */ + public Component getComponent(int i) { + return (Component) m_components.get(i); + } + + /** + * Gets a page component by key. + * + * @pre s != null + */ + Component getComponent(String s) { + return (Component) m_componentMap.get(s); + } + + /** + * Gets the form model that contains the parameters for the page's state. + */ + public final FormModel getStateModel() { + return m_stateModel; + } + + /** + * Gets the ParameterModels held in this Page. + * + * @return an iterator of ParameterModels. + */ + public Iterator getParameters() { + return m_stateModel.getParameters(); + } + + /** + * Checks whether the specified component is visible by default on the page. + * + * @param c a component contained in the page + * + * @return true if the component is visible by default; + * false otherwise. + * + * @see #setVisibleDefault setVisibleDefault + * @see Component#setVisible Component.setVisible + */ + public boolean isVisibleDefault(Component c) { + Assert.isTrue(stateContains(c)); + + return !m_invisible.get(stateIndex(c)); + } + + /** + * Sets whether the specified component is visible by default. The default + * visibility is used when a page is displayed for the first time and on + * subsequent requests until the visibility of a component is changed + * explicitly with {@link Component#setVisible + * Component.setVisible}. + * + *

+ * When a component is first added to a page, it is visible. + * + * @param c a component whose visibility is to be set + * @param v true if the component is visible; + * false otherwise. + * + * @see Component#setVisible Component.setVisible + * @see Component#register Component.register + */ + public void setVisibleDefault(Component c, boolean v) { + Assert.isUnlocked(this); + + addComponent(c); + int i = stateIndex(c); + if (v) { + m_invisible.clear(i); + } else { + m_invisible.set(i); + } + } + + /** + * The global name of the parameter name in the component + * c. + */ + public String parameterName(Component c, String name) { + if (c == null || !stateContains(c)) { + return name; + } + + return componentPrefix(c) + name; + } + + /** + * The global name of the parameter name. + */ + public String parameterName(String name) { + return parameterName(null, name); + } + + void reset(final PageState ps, Component cmpnt) { + Traversal resetter = new Traversal() { + + @Override + protected void act(Component c) { + Collection cp = getComponentParameters(c); + if (cp != null) { + Iterator iter = cp.iterator(); + while (iter.hasNext()) { + ParameterModel p = (ParameterModel) iter.next(); + ps.setValue(p, null); + } + } + c.setVisible(ps, isVisibleDefault(c)); + } + + }; + resetter.preorder(cmpnt); + } + + /** + * Return the prefix that is prepended to each component's state parameters + * to keep them unique. + */ + private final String componentPrefix(Component c) { + if (c == null) { + return COMPONENT_PREFIX + "g" + DELIMITER; + } else { + // WRS: preferentially use key if it exists + String key = c.getKey(); + if (key == null) { + if (stateContains(c)) { + key = String.valueOf(stateIndex(c)); + } else { + throw new IllegalArgumentException( + "Cannot generate prefix for component: key is null " + + "and component " + c.toString() + "/" + c.getKey() + + " did not register with page."); + } + } + return COMPONENT_PREFIX + key + DELIMITER; + } + } + + /** + * Undo the name change that {@link #parameterName} does. + * + * @param name a possibly mangled name + * + * @return the unmangled name. + */ + private static final String unmangle(String name) { + if (!name.startsWith(COMPONENT_PREFIX)) { + return name; + } + // Find the second occurence of delimiter + int prefix = name.indexOf(DELIMITER, name.indexOf(DELIMITER) + 1); + if (prefix >= 0 && prefix < name.length()) { + return name.substring(prefix + 1); + } + return name; + } + + // Procs for debugging output + private static String NAME = "name"; + + /** + * Produces an XML fragment that captures the layout of this page. + */ + private void showStructure(PageState s, Element root) { + final HttpServletRequest req = s.getRequest(); + Element state = root.newChildElement("bebop:state", BEBOP_XML_NS); + // Selected component + String sel = req.getParameter(m_selected.getName()); + Element selected = state.newChildElement("bebop:selected", BEBOP_XML_NS); + + selected.addAttribute(NAME, m_selected.getName()); + selected.setText(sel); + + // Control event + Element eventName = state.newChildElement("bebop:eventName", + BEBOP_XML_NS); + eventName.addAttribute(NAME, m_controlEvent.getName()); + eventName.setText(req.getParameter(m_controlEvent.getName())); + Element eventValue = state.newChildElement("bebop:eventValue", + BEBOP_XML_NS); + eventValue.addAttribute(NAME, m_controlValue.getName()); + eventValue.setText(req.getParameter(m_controlValue.getName())); + + // Global parameters + Element globalState = root.newChildElement("bebop:params", BEBOP_XML_NS); + for (Iterator ii = getStateModel().getParameters(); ii.hasNext();) { + ParameterModel p = (ParameterModel) ii.next(); + if (!p.getName().startsWith(COMPONENT_PREFIX)) { + Element param = globalState.newChildElement("bebop:param", + BEBOP_XML_NS); + param.addAttribute(NAME, p.getName()); + param.setText(String.valueOf(s.getValue(p))); + } + } + + showVisibility(s, this, root); + } + + /** + * @see showStructure(PageState, Element) + */ + private void showVisibility(PageState s, Component c, Element parent) { + HttpServletRequest req = s.getRequest(); + + Element cmp = parent.newChildElement("bebop:component", BEBOP_XML_NS); + cmp.addAttribute(NAME, getDebugLabel(c)); + cmp.addAttribute("idx", String.valueOf(stateIndex(c))); + cmp.addAttribute("isVisible", (s.isVisible(c) ? "yes" : "no")); + cmp.addAttribute("class", c.getClass().getName()); + + if (c.getKey() != null) { + String prefix = componentPrefix(c); + for (Iterator i = getStateModel().getParameters(); i.hasNext();) { + ParameterModel p = (ParameterModel) i.next(); + if (!p.getName().startsWith(prefix)) { + continue; + } + + Element param = parent.newChildElement("bebop:param", + BEBOP_XML_NS); + param.addAttribute(NAME, unmangle(p.getName())); + param.addAttribute("defaultValue", + String.valueOf(req.getParameter(p.getName()))); + param + .addAttribute("currentValue", String.valueOf(s.getValue(p))); + } + } + for (Iterator i = c.children(); i.hasNext();) { + showVisibility(s, ((Component) i.next()), cmp); + } + } + + private static String getDebugLabel(Component c) { + if (c.getKey() != null) { + return c.getKey(); + } + + String klass = c.getClass().getName(); + return klass.substring(klass.lastIndexOf(".") + 1, klass.length()); + } + + /** + * return a string that represents an ordered list of component ids used on + * the page. For situations where only the components present is of + * importance, this may be used by implementations of hashCode & equals + * + * @return + */ + public String getComponentString() { + Iterator it = m_componentMap.keySet().iterator(); + /*int hash = 0; + while (it.hasNext()) { + String componentId = (String)it.next(); + s_log.debug("component id = " + componentId); + hash = hash | componentId.hashCode(); + s_log.debug("hash so far = " + hash); + }*/ + Date start = new Date(); + + StringBuilder hashString = new StringBuilder(); + while (it.hasNext()) { + String componentId = (String) it.next(); + hashString.append(componentId); + } + s_log.debug("Time to create hashCode for page: " + (new Date().getTime() + - start. + getTime())); + return hashString.toString(); + + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/PageErrorDisplay.java b/ccm-core/src/main/java/com/arsdigita/bebop/PageErrorDisplay.java new file mode 100755 index 000000000..e424804e6 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/PageErrorDisplay.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.list.ListModel; +import com.arsdigita.bebop.list.ListModelBuilder; +import com.arsdigita.bebop.list.ListCellRenderer; +import com.arsdigita.util.LockableImpl; +import com.arsdigita.xml.Element; +import com.arsdigita.globalization.GlobalizedMessage; + +import java.util.Iterator; + +/** + * Displays validation errors for the page. These might have occured due to validation listeners on + * some state parameters within the page. + * + * @author Stanislav Freidin + * @version $Id: PageErrorDisplay.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class PageErrorDisplay extends List { + + private static final String COLOR = "color"; + + /** + * Constructs a new PageErrorDisplay. + */ + public PageErrorDisplay() { + this(new PageErrorModelBuilder()); + } + + /** + * Constructs a new PageErrorDisplay from the errors supplied by a list model + * builder. + * + * @param builder the {@link ListModelBuilder} that will supply the errors + * + */ + protected PageErrorDisplay(ListModelBuilder builder) { + super(builder); + setCellRenderer(new LabelCellRenderer()); + setTextColor("red"); + setClassAttr("pageErrorDisplay"); + } + + /** + * Sets the HTML color of the error messages. + * + * @param c An HTML color, such as "#99CCFF" or "red" + */ + public void setTextColor(String c) { + setAttribute(COLOR, c); + } + + /** + * Gets the HTML color of the error messages. + * + * @return the HTML color of the error messages. + */ + public String getTextColor() { + return getAttribute(COLOR); + } + + /** + * Determines if there are errors to display. + * + * @param state the current page state + * + * @return true if there are any errors to display; false otherwise. + */ + protected boolean hasErrors(PageState state) { + return (state.getErrors().hasNext()); + } + + /** + * Generates the XML for this component. If the state has no errors in it, does not generate any + * XML. + * + * @param state the current page state + * @param parent the parent XML element + */ + public void generateXML(PageState state, Element parent) { + if (hasErrors(state)) { + super.generateXML(state, parent); + } + } + + // A private class which builds a ListModel based on form errors + private static class PageErrorModelBuilder extends LockableImpl + implements ListModelBuilder { + + public PageErrorModelBuilder() { + super(); + } + + public ListModel makeModel(List l, PageState state) { + return new StringIteratorModel(state.getErrors()); + } + + } + + // A ListModel which generates items based on an Iterator + protected static class StringIteratorModel implements ListModel { + + private Iterator m_iter; + private GlobalizedMessage m_error; + private int m_i; + + public StringIteratorModel(Iterator iter) { + m_iter = iter; + m_error = null; + m_i = 0; + } + + public boolean next() { + if (!m_iter.hasNext()) { + m_i = 0; + return false; + } + + m_error = (GlobalizedMessage) m_iter.next(); + ++m_i; + + return true; + } + + private void checkState() { + if (m_i == 0) { + throw new IllegalStateException( + "next() has not been called succesfully" + ); + } + } + + public Object getElement() { + checkState(); + return m_error; + } + + public String getKey() { + checkState(); + return Integer.toString(m_i); + } + + } + + // A ListCellRenderer that renders labels + private static class LabelCellRenderer implements ListCellRenderer { + + public Component getComponent(List list, PageState state, Object value, + String key, int index, boolean isSelected) { + return new Label((GlobalizedMessage) value); + } + + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/PageState.java b/ccm-core/src/main/java/com/arsdigita/bebop/PageState.java new file mode 100644 index 000000000..137c5d5c4 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/PageState.java @@ -0,0 +1,1075 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.parameters.BitSetParameter; +import com.arsdigita.bebop.parameters.ParameterData; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.bebop.util.Traversal; +import com.arsdigita.dispatcher.DispatcherHelper; +import com.arsdigita.util.Assert; +import com.arsdigita.util.UncheckedWrapperException; +import com.arsdigita.web.ParameterMap; +import com.arsdigita.web.RedirectSignal; +import com.arsdigita.web.URL; +import com.arsdigita.web.Web; +import com.arsdigita.xml.Element; +import java.io.IOException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import org.apache.log4j.Logger; + +/** + *

The request-specific data (state) for a {@link Page}. All + * methods that need access to the state of the associated page during + * the processing of an HTTP request are passed a + * PageState object. Since PageState + * contains request-specific data, it should only be live during the + * servicing of one HTTP request. This class has several related + * responsibilites:

+ * + *

State Management

+ * + *

PageState objects store the values for global and + * component state parameters and are responsible for retrieving these + * values from the HTTP request. Components can access the values of + * their state parameters through the {@link #getValue getValue} and + * {@link #setValue setValue} methods.

+ * + *

This class is also responsible for serializing the current state of + * the page in a variety of ways for inclusion in the output: {@link + * #generateXML generateXML} adds the state to an XML element and {@link + * #stateAsURL stateAsURL} encodes the page's URL.

+ * + *

The serialized form of the current page state can be quite large + * so the page state can also be preserved in the HttpSession, on the + * server. The Page object specifies this by calling {@link + * Page#setUsingHttpSession setUsingHttpSession(true)}. When this + * flag is set, then the page state will be preserved in the + * HttpSession, and the {@link #stateAsURL stateAsURL} method will + * only serialize the current URL and the control event. It will also + * include a URL variable that the subsequent request uses to retrieve + * the correct page state from the HttpSession. If the page state for + * a particular request cannot be found, the constructor will throw a + * {@link SessionExpiredException SessionExpiredException}.

+ * + *

Up to {@link #getMaxStatesInSession getMaxStatesInSession()} + * independent copies of the page state may be stored in the + * HttpSession, to preserve the behavior of the browser's "back" + * button.

+ * + *

Note: As a convention, only the component to which a + * state parameter belongs should modify it by calling + * getValue and setValue. All other objects + * should manipulate the state of a component only through + * well-defined methods on the component.

+ * + *

Control Events

+ * + *

The control event consists of a pair of strings, the name + * of the event and its associated value. Components use the + * control event to send themselves a "delayed signal", i.e. a signal + * that is triggered when the same page is requested again through a + * link that has been generated with the result of {@link #stateAsURL} + * as the target. The component can then access the name and value of + * the event with calls to {@link #getControlEventName} and {@link + * #getControlEventValue}.

+ * + *

Typically, a component will contain code corresponding to the following + * in its generateXML method: + *

+ *  public void generateXML(PageState state, Element parent) {
+ *  if ( isVisible(state) ) {
+ *      MyComponent target = firePrintEvent(state);
+ *      Element link = new Element ("bebop:link", BEBOP_XML_NS);
+ *      parent.addContent(link);
+ *      target.generateURL(state, link);
+ *      target.exportAttributes(link);
+ *      target.generateExtraXMLAttributes(state, link);
+ *      target.getChild().generateXML(state, link);
+ *  }
+ *  }
+ * 
+ * (In reality, the component would not write a bebop:link element + * directly, but use a {@link ControlLink} to do that automatically.) + *

When the user clicks on the link that the above generateXML + * method produces, the component's respond method is called + * and can access the name and value of the event with code similar to the + * following: + *

+ *   public void respond(PageState state) {
+ *     String name = state.getControlEventName();
+ *     String value = state.getControlEventValue();
+ *     if ( "name".equals(name) ) {
+ *       doSomeStateChange(value);
+ *     } else {
+ *       throw new IllegalArgumentException("Can't understand event " + name);
+ *     }
+ *   }
+ * 
+ * + * + *

Temporary Storage

+ * + *

Request local variables make it possible to store arbitrary objects + * in the page state and allow components (and other objects) to cache + * results of previous computations. For example, the {@link Form} + * component uses a request local variable to store the {@link FormData} it + * generates from the request and make it acessible to the widgets it + * contains, and to other components through calling {@link + * Form#getFormData Form.getFormData(state)}. See the documentation for + * {@link RequestLocal} on how to use your own request local variables. + * + *

Convenience Access to Related Objects

+ * + *

PageState objects store references to the HTTP request + * and response that is currently being served. Components are free to + * manipulate these objects as they see fit. + *

+ * + * @author David Lutterkort + * @author Uday Mathur + * @version $Id$ + */ +public class PageState { + + /** Class specific logger instance. */ + private static final Logger s_log = Logger.getLogger(PageState.class); + + /** The underlying Page object. */ + private Page m_page; + + /** + * The request to which this object corresponds + */ + private HttpServletRequest m_request; + + /** + * The response to which results will be sent. + */ + private HttpServletResponse m_response; + + /** + * The values of global and component specific state parameters extracted + * from the request. + */ + private FormData m_pageState; + + /** + * Temporary storage of arbitrary objects. + */ + private Map m_attributes; + + /** + * The component that currently holds exclusive access to the control + * event. Usually null, unlesss a component calls {@link + * #grabControlEvent}. + */ + private Component m_grabbingComponent; + + /** + * The visibility state of components. For a component with n = + * Page.stateIndex, the n-th bit is set if the component is not + * visible. + * + *

Initially, this variable refers to Page.m_invisible. Only when a + * call to {@link #setVisible} is made, is that value + * copied. (Copy-on-write) + */ + private BitSet m_invisible; + private boolean m_visibilityDirty = true; + + private int m_nextSession; + + private final static String SESSION_ATTRIBUTE = + "com.arsdigita.bebop.FormData"; + private final static String SESSION_COUNTER_ATTRIBUTE = + "com.arsdigita.bebop.FormData.counter"; + private final static String CURRENT_SESSION_PARAMETER = + "bbp.session"; + private final static String PAGE_STATE_ATTRIBUTE = + "com.arsdigita.bebop.PageState"; + + private static int s_maxSessions = 10; + + private Traversal m_visibilityTraversal = new VisibilityTraversal(); + private List m_visibleComponents; + + /** + * Returns the maximum number of independent page states that may be stored + * in the HttpSession, for preserving the behavior of the "back" button. + * @return the maximum number of independent page states that may + * be stored in the HttpSession. + */ + public static int getMaxStatesInSession() { + return s_maxSessions; + } + + /** + * Sets the maximum number of independent page states that may be stored in + * the HttpSession, for preserving the behavior of the "back" button. + * @param x the maximum number of independent page states to store + * in the HttpSession. + */ + public static void setMaxStatesInSession(int x) { + s_maxSessions = x; + } + + /** + * Returns the page state object for the given request, or null if none + * exists yet. + * + * @param request The servlet request. + * + * @return The page state object for the given request, or null if none + * exists yet. + **/ + public static PageState getPageState(HttpServletRequest request) { + return (PageState) request.getAttribute(PAGE_STATE_ATTRIBUTE); + } + + /** + * Returns the page state object for the current request, or null if none + * exists yet. + * + * @return The page state object for the current request, or null if none + * exists yet. + **/ + + public static PageState getPageState() { + HttpServletRequest request = DispatcherHelper.getRequest(); + + if (request == null) { + return null; + } else { + return getPageState(request); + } + } + + /** + * Construct the PageState for an HTTP request. + * + * Calls {@link FormModel#process process} on the form model underlying + * the page model and calls {@link Component#respond respond} on the + * component from which the request originated. + * + * @param page The model of the page + * @param request The request being served + * @param response Where the response should be sent + * + * @pre request != null && request.get(page.PAGE_SELECTED) != null + * @pre response != null && ! response.isCommitted() + * @pre page != null + * + */ + public PageState(Page page, HttpServletRequest request, + HttpServletResponse response) + throws ServletException { + m_page = page; + + if ( m_page == null ) { + m_page = new Page(); + m_page.lock(); + } + + m_request = request; + m_response = response; + + FormData pageStateFromSession = null; + if (m_page.isUsingHttpSession()) { + pageStateFromSession = getStateFromSession(); + } + + // always treat the request as a submission + m_pageState = new FormData(m_page.getStateModel(), request, true, + pageStateFromSession); + m_page.getStateModel().process(this, m_pageState); + m_invisible = decodeVisibility(); + + // Add the PageState to the request + m_request.setAttribute(PAGE_STATE_ATTRIBUTE, this); + } + + + protected BitSet decodeVisibility() { + BitSet difference = (BitSet)m_pageState.get(Page.INVISIBLE); + BitSet current = (BitSet)m_page.m_invisible.clone(); + if (difference != null) { + current.xor(difference); + } + return current; + } + + protected BitSet encodeVisibility(BitSet current) { + BitSet difference = (BitSet)m_page.m_invisible.clone(); + difference.xor(current); + return difference; + } + + /** + * Helper function that returns the PageState object from the + * HttpSession. + */ + private FormData getStateFromSession() throws SessionExpiredException { + HttpSession session = m_request.getSession(true); + FormData state = null; + + // have to bootstrap this manually from the request + String key = + m_request.getParameter(CURRENT_SESSION_PARAMETER); + if (key != null) { + state = (FormData)session.getAttribute(SESSION_ATTRIBUTE + key); + // session is expired if we're looking for a particular + // page state and couldn't find it. + if (state == null) { + throw new SessionExpiredException(); + } + } + return state; + } + + /** + * Process the PageState and fire necessary events. Call respond on the + * selected bebop component. + */ + public void respond() throws ServletException { + String compKey = (String) m_pageState.get(Page.SELECTED); + + if ( compKey != null ) { + Component c = m_page.getComponent(compKey); + if ( c == null ) { + throw new ServletException("Selected component not on page."); + } + try { + c.respond(this); + } finally { + } + } + } + + /** + * Return the page for which this object holds the state. + * + * @return the page for which this object holds state. + * @post return != null + */ + public final Page getPage() { + return m_page; + } + + + /** + * Return the request object for the HTTP request. + * + * @return The HTTP request being currently served. + */ + public final HttpServletRequest getRequest() { + return m_request; + } + + /** + * Return the response object for the HTTP response. + * + * @return The response for the HTTP request being served + */ + public final HttpServletResponse getResponse() { + return m_response; + } + + /** + * The index of a component in the page model + * + * @pre c != null + */ + private int indexOf(Component c) { + return m_page.stateIndex(c); + } + + /** + * Return true is the comonent c is currently + * visible. This method should only be used by components + * internally. All other objects should call {@link Component#isVisible + * Component.isVisible} on the component. + * + * @param c the components whose visibility should be returned + * @return true if the component is visible + */ + public boolean isVisible(Component c) { + if ( ! getPage().stateContains(c)) { + return true; + } + if ( m_invisible == null ) { + return m_page.isVisibleDefault(c); + } else { + return ! m_invisible.get(indexOf(c)); + } + } + + /** + * Return true is the component c is currently + * visible on the page displayed to the end user. This is true + * if the component's visibility flag is set, and it is in a + * container visible to the end user. + * + *

This method is different than isVisible which + * only returns the visibility flag for the individual component. + * + * @param c the components whose visibility should be returned + * @return true if the component is visible + */ + public boolean isVisibleOnPage(Component c) { + if (m_visibleComponents == null) { + m_visibleComponents = new ArrayList(); + m_visibilityTraversal.preorder(getPage().getPanel()); + } + + return m_visibleComponents.contains(c); + } + + private class VisibilityTraversal extends Traversal { + protected void act(Component c) { + m_visibleComponents.add(c); + } + + protected int test(Component c) { + if (isVisible(c)) { + return PERFORM_ACTION; + } else { + // not visible, so neither are children + return SKIP_SUBTREE; + } + } + } + + + /** + * Set the visibility of a component. Calls to this method change the + * default visibility set with {@link Page#setVisibleDefault + * Page.setVisibleDefault}. This method should only be used by + * components internally. All other objects should call {@link + * Component#setVisible Component.setVisible} on the component. + * + *

Without explicit changes, a component is visible. + * + * @param c the component whose visibility is to be changed + * @param v true if the component should be visible + */ + public void setVisible(final Component c, final boolean v) { + if (Assert.isEnabled()) { + Assert.isTrue(getPage().stateContains(c), + "Component" + c + " is not registered on Page " + + getPage()); + } + + if (m_invisible == null || m_invisible == getPage().m_invisible) { + // copy on write + m_invisible = (BitSet) getPage().m_invisible.clone(); + } + + int i = indexOf(c); + + if (v) { + if (!m_invisible.get(i)) + return; + m_invisible.clear(i); + } else { + if (m_invisible.get(i)) + return; + m_invisible.set(i); + } + if (s_log.isInfoEnabled()) { + s_log.info("Marking visibility parameter as dirty " + m_request + " because of component " + c); + } + // Do this only in toURL since the RLE is expensive + //m_pageState.put(Page.INVISIBLE, encodeVisibility(m_invisible)); + m_visibilityDirty = true; + m_visibleComponents = null; + } + + /** + * Resets the given component and its children to their default + * visibility. Also resets the state parameters of the given + * component and its children to null. This is not a speedy + * method. Do not call gratuitously. + * + * @param c the parent component whose state parameters and + * visibility you wish to reset. + * */ + public void reset(Component c) { + getPage().reset(this, c); + } + +// /** +// * Store an attribute keyed on the object key. The +// * PageState puts no restrictions on what can be stored as +// * an attribute or how they are managed. +// * +// * To remove an attribute, call setAttribute(key, null). +// * +// * The attributes are only accessible as long as the +// * PageState is alive, typically only for the duration of +// * the request. +// * +// * @deprecated Use either setAttribute on {@link +// * HttpServletRequest the HTTP request object}, or, preferrably, use a +// * {@link RequestLocal request local} variable. Will be removed on +// * 2001-06-13. +// * +// */ +// public void setAttribute(Object key, Object value) { +// if ( m_attributes == null ) { +// m_attributes = new HashMap(); +// } +// m_attributes.put(key, value); +// } + +// /** +// * Get the value of an attribute stored with the same key with {@link +// * #setAttribute setAttribute}. +// * +// * @deprecated Use either getAttribute on {@link +// * HttpServletRequest the HTTP request object}, or, preferrably, use a +// * {@link RequestLocal request local} variable. Will be removed on +// * 2001-06-13. +// * +// */ +// public Object getAttribute(Object key) { +// if ( m_attributes == null ) { +// return null; +// } +// return m_attributes.get(key); +// } + + /** + * Set the value of the state parameter p. The concrete + * type of value must be compatible with the type of the + * state parameter. + * + *

The parameter must have been previously added with a call to + * {@link Page#addComponentStateParam + * Page.getComponentStateParam}. This method should only be called by + * the component that added the state parameter to the page. Users of + * the component should manipulate the parameter through methods the + * component provides. + * + * @param p a state parameter + * @param value the new value for this state parameter. The concrete + * type depends on the type of the parameter being used. + */ + public void setValue(ParameterModel p, Object value) { + m_pageState.put(p.getName(), value); + } + + /** + * Get the value of state parameter p. The concrete type of + * the return value depends on the type of the parameter being + * used. + * + *

The parameter must have been previously added with a call to + * {@link Page#addComponentStateParam + * Page.addComponentStateParam}. This method should only be called by + * the component that added the state parameter to the page. Users of + * the component should manipulate the parameter through methods the + * component provides. + * + * @param p a state parameter + * @return the current value for this state parameter. The concrete + * type depends on the type of the parameter being used. + */ + public Object getValue(ParameterModel p) { + return m_pageState.get(p.getName()); + } + + /** + * Get the value of a global state parameter. + * @deprecated Use {@link #getValue(ParameterModel m)} instead. If you + * don't have a reference to the parameter model, you should not be + * calling this method. Instead, the component that registered the + * parameter should provide methods to manipulate it. Will be removed + * 2001-06-20. + */ + public Object getGlobalValue(String name) { + // WRS 9/7/01 + // work-around: m_pageState will throw an exception when we get + // a named parameter that's not in the form model. But for + // API stability, we need to trap this and return null; since + // there's no way to tell what set of globalValues are legal + // in any particular page--that's what ParameterModels are for. + try { + return m_pageState.get(m_page.parameterName(name)); + } catch (IllegalArgumentException iae) { + return null; + } + } + +// /** +// * Change the value of a global parameter +// * +// * @deprecated Use {@link #setValue(ParameterModel m, Object o)} +// * instead. If you don't have a reference to the parameter model, you +// * should not be calling this method. Instead, the component that +// * registered the parameter should provide methods to manipulate +// * it. Will be removed 2001-06-20. +// */ +// public void setGlobalValue(String name, Object value) { +// m_pageState.put(m_page.parameterName(name), value); +// } + + // Handling the control event + + /** + * Grab the control event. Until {@link #releaseControlEvent + * releaseControlEvent(c)} is called, only the component c + * can be used in calls to {@link #setControlEvent setControlEvent}. + * @pre c != null + */ + public void grabControlEvent(Component c) { + if ( m_grabbingComponent != null && m_grabbingComponent != c ) { + throw new IllegalStateException + ("Component " + m_grabbingComponent.toString() + + " already holds the control event"); + } + m_grabbingComponent = c; + } + + /** + * Set the control event. The control event is a delayed event + * that only gets acted on when another request to this Page + * is made. It is used to set which component should receive the + * submission and lets the component set one component-specific name-value + * pair to be used in the submission. + *

+ * After calling this method links and hidden form controls generated + * with {@link #stateAsURL} have been amended so that if the user clicks such a + * link or submits a form containing those hidden controls, the exact + * same values can be retrieved with {@link #getControlEventName} and + * {@link #getControlEventValue}. + *

+ * Stateful components can use the control event to change their + * state. For example, a tabbed pane t might call + * setControlEvent(t, "select", "2") just prior to + * generating the link for its second tab. + *

+ * The values of name and value have no + * meaning to the page state, they are simply passed through without + * modifications. It is up to specific components what values of + * name and value are meaningful for it. + * + * @param c + * @param name The component specific name of the event, may be + * null + * @param value The component specific value of the event, may be + * null + * @pre c == null || getPage().stateContains(c) + */ + public void setControlEvent(Component c, String name, String value) { + Assert.isTrue(c == null || getPage().stateContains(c), + "c == null || getPage().stateContains(c)"); + if ( m_grabbingComponent != null && m_grabbingComponent != c ) { + throw new IllegalStateException + ("Component " + m_grabbingComponent.toString() + + " holds the control event"); + } + // FIXME: This needs to take named components into account + String key = null; + if (c != null) { + key = c.getKey(); + if (key == null) { + key = Integer.toString(m_page.stateIndex(c)); + } + } + m_pageState.put(Page.SELECTED, + (c == null) ? null : key); + m_pageState.put(Page.CONTROL_EVENT, name); + m_pageState.put(Page.CONTROL_VALUE, value); + } + + /** + * Set the control event. Both the event name and its value will be + * null. + */ + public void setControlEvent(Component c) { + setControlEvent(c, null, null); + } + + /** + * Clear the control event. Links and hidden form variables generated + * after this call will not cause any component's respond method to be + * called. + * + * @throws IllegalStateException if any component has grabbed the + * control event but not released it yet. + */ + public void clearControlEvent() { + setControlEvent(null); + } + + /** + * Get the name of the control event. + */ + public String getControlEventName() { + return (String) m_pageState.get(Page.CONTROL_EVENT); + } + + /** + * Get the value associated with the control event. + */ + public String getControlEventValue() { + return (String) m_pageState.get(Page.CONTROL_VALUE); + } + + /** + * Release the control event. + * @param c The component that was passed to the last call to + * grabControlEvent + * @pre getPage().stateContains(c) + */ + public void releaseControlEvent(Component c) { + if ( m_grabbingComponent == null ) { + throw new IllegalStateException + ("No component holds the control event, but " + c.toString() + + " tries to release it."); + } + if ( c != m_grabbingComponent ) { + throw new IllegalStateException + ("Component " + m_grabbingComponent.toString() + + " holds the control event, but " + c.toString() + + " tries to release it."); + } + m_grabbingComponent = null; + } + + /** + * Add elements to parent that represent the current page + * state. For each component or global state parameter on the page, a + * <bebop:pageState> element is added to + * parent. The name and value attributes + * of the element contain the name and value of the state parameters as + * they should appear in an HTTP request made back to this page. + * + *

Generates DOM fragment: + *

+     * <bebop:pageState name=... value=.../>
+     * 
+ * + * @param form This is the form in which the hidden variables will exist + * + * @see #setControlEvent setControlEvent + */ + public void generateXML(Element parent) { + synchronizeVisibility(); + + for ( Iterator i = m_pageState.getParameters().iterator(); + i.hasNext(); ) { + ParameterData p = (ParameterData) i.next(); + + String key = (String) p.getName(); + String value = p.marshal(); + + if ( value != null ) { + Element hidden = parent.newChildElement("bebop:pageState", + Component.BEBOP_XML_NS); + hidden.addAttribute("name", key); + hidden.addAttribute("value", value); + } + } + } + + public void generateXML(Element parent, Iterator models) { + synchronizeVisibility(); + + List excludeParams = new ArrayList(); + if (models != null) { + while (models.hasNext()) { + excludeParams.add(((ParameterModel)models.next()).getName()); + } + } + + for ( Iterator i = m_pageState.getParameters().iterator(); + i.hasNext(); ) { + ParameterData p = (ParameterData) i.next(); + + String key = (String) p.getName(); + String value = p.marshal(); + + if (value == null || excludeParams.contains(key)) { + continue; + } + + Element hidden = parent.newChildElement("bebop:pageState", + Component.BEBOP_XML_NS); + hidden.addAttribute("name", key); + hidden.addAttribute("value", value); + } + } + + /** + * Export the current page state into the HttpSession by putting the entire + * m_pageState (type FormData) object into the HttpSession. + * + *

+ * Package visibility is intentional. + * + * @see #setControlEvent setControlEvent */ + void stateAsHttpSession() { + // create session if we need to + HttpSession session = m_request.getSession(true); + // get + increment counter counter + Integer counterObj = + (Integer)session.getAttribute(SESSION_COUNTER_ATTRIBUTE); + if (counterObj == null) { + m_nextSession = 0; + } else { + m_nextSession = counterObj.intValue() + 1; + } + + session.setAttribute(SESSION_ATTRIBUTE + m_nextSession, m_pageState); + session.setAttribute(SESSION_COUNTER_ATTRIBUTE, + new Integer(m_nextSession)); + // remove an old session + int toRemove = m_nextSession - s_maxSessions; + if (toRemove >= 0) { + session.removeAttribute(SESSION_ATTRIBUTE + toRemove); + } + } + + /** + *

Write the current state of the page as a URL.

+ * + *

The URL representing the state points to the same URL that + * the current request was made from and contains a query string + * that represents the page state.

+ * + *

If the current page has the useHttpSession flag set, then + * the URL query string that we generate will only contain the + * current value of the control event, and the rest of the page + * state is preserved via the HttpSession. Otherwise, the query + * string contains the entire page state.

+ * + * @return a string containing the current state of a page. + * @see #setControlEvent setControlEvent + * @see Page#isUsingHttpSession Page.isUsingHttpSession + * @see Page#setUsingHttpSession Page.setUsingHttpSession + */ + public String stateAsURL() throws IOException { + return m_response.encodeURL(toURL().toString()); + } + + public final URL toURL() { + synchronizeVisibility(); + + final ParameterMap params = new ParameterMap(); + + if (s_log.isDebugEnabled()) { + dumpVisibility(); + } + + final Iterator iter = m_pageState.getParameters().iterator(); + + while (iter.hasNext()) { + final ParameterData data = (ParameterData) iter.next(); + + final String key = (String) data.getName(); + + if (!m_page.isUsingHttpSession() + || Page.CONTROL_EVENT_KEYS.contains(key)) { + final String value = data.marshal(); + + if (value != null) { + params.setParameter(key, value); + } + } + + } + + if (m_page.isUsingHttpSession()) { + params.setParameter(CURRENT_SESSION_PARAMETER, + new Integer(m_nextSession)); + } + + return URL.request(m_request, params); + } + + + private void synchronizeVisibility() { + if (m_visibilityDirty) { + if (s_log.isInfoEnabled()) { + s_log.info("Encoding visibility parameter " + m_request); + } + m_pageState.put(Page.INVISIBLE, encodeVisibility(m_invisible)); + m_visibilityDirty = false; + } + } + + private void dumpVisibility() { + BitSetParameter raw = new BitSetParameter("raw", BitSetParameter.ENCODE_RAW); + BitSetParameter dgap = new BitSetParameter("dgap", BitSetParameter.ENCODE_DGAP); + + BitSet current = (BitSet)m_invisible; + BitSet base = (BitSet)m_page.m_invisible; + + BitSet difference = (BitSet)current.clone(); + difference.xor(base); + + s_log.debug("Current: " + current.toString()); + s_log.debug("Default: " + base.toString()); + s_log.debug("Difference: " + difference.toString()); + + s_log.debug("Current RAW: " + raw.marshal(current)); + s_log.debug("Default RAW: " + raw.marshal(base)); + s_log.debug("Difference RAW: " + raw.marshal(difference)); + + s_log.debug("Current DGAP: " + dgap.marshal(current)); + s_log.debug("Default DGAP: " + dgap.marshal(base)); + s_log.debug("Difference DGAP: " + dgap.marshal(difference)); + + s_log.debug("Current Result: " + dgap.unmarshal(dgap.marshal(current))); + s_log.debug("Default Result: " + dgap.unmarshal(dgap.marshal(base))); + s_log.debug("Difference Result: " + dgap.unmarshal(dgap.marshal(difference))); + + if (!current.equals(dgap.unmarshal(dgap.marshal(current)))) { + s_log.debug("Broken marshal/unmarshal for current"); + } + if (!base.equals(dgap.unmarshal(dgap.marshal(base)))) { + s_log.debug("Broken marshal/unmarshal for default"); + } + if (!difference.equals(dgap.unmarshal(dgap.marshal(difference)))) { + s_log.debug("Broken marshal/unmarshal for difference"); + } + } + + /** + * Get the URI to which the current request was made. Copes with the + * black magic that is needed to get the URI if the request was handled + * through a dispatcher. If no dispatcher was involved in the request, + * returns the request URI from the HTTP request. + * + * @post return != null + * + * @return the URI to which the current request was made + */ + public String getRequestURI() { + final URL url = Web.getWebContext().getRequestURL(); + + if (url == null) { + return m_request.getRequestURI(); + } else { + return url.getRequestURI(); + } + } + + /** + * Return true if all the global and component state parameters + * extracted from the HTTP request were successfully validated against + * their parameter models in the {@link Page}. + * + * @return true if the values of all global and component state + * parameters are valid with respect to their parameter models. + */ + public boolean isValid() { + return m_pageState.isValid(); + } + + /** + * Return an iterator over the errors that occurred in trying to + * validate the state parameters against their parameter models in + * {@link Page}. + * + * @return an iterator over validation errors + * @see FormData#getErrors + */ + public Iterator getErrors() { + return m_pageState.getAllErrors(); + } + + /** + * Return a string with all the errors that occurred in trying to + * validate the state parameters against their parameter models in + * {@link Page}. The string consists simply of the concatenation of all + * error messages that the result of {@link #getErrors} iterates over. + * + * @return all validation errors concatenated into one string + */ + public String getErrorsString() { + StringBuffer s = new StringBuffer(); + for (Iterator i = m_pageState.getAllErrors(); i.hasNext(); ) { + s.append(i.next().toString()); + s.append(System.getProperty("line.separator")); + } + return s.toString(); + } + + /** + * Force the validation of all global and component state parameters + * against their parameter models. This method only needs to be called if + * the values of the parameters have been changed with {@link #setValue + * setValue} or {@link #setGlobalValue setGlobalValue} and may now + * contain invalid values. + */ + public void forceValidate() { + m_pageState.forceValidate(this); + } + + /** Convert to a String. + * @return a human-readable representation of this. + */ + public String toString() { + String newLine = System.getProperty("line.separator"); + String result = + super.toString() + " = {" + newLine + + "m_page = " + m_page + "," + newLine + + "m_request = " + m_request + "," + newLine + + "m_response = " + m_response + "," + newLine + // FormData + + "m_pageState = " + m_pageState.asString() + "," + newLine + + "m_attributes = " + m_attributes + "," + newLine + // Map to FormData + + "," + newLine + + "m_grabbingComponent = " + m_grabbingComponent + "," + newLine + + "m_invisible = " + m_invisible + newLine + + "}"; + return result; + } + + /** + * Clear the control event then redirect to the new page state. + * + * @param isCommitRequested indicates if a commit required before the redirect + * + * @throws RedirectSignal to the new page state + * + * @see RedirectSignal#RedirectSignal(String, boolean) + */ + public void redirectWithoutControlEvent(boolean isCommitRequested) { + clearControlEvent(); + try { + throw new RedirectSignal(stateAsURL(), true); + } catch (IOException ioe) { + throw new UncheckedWrapperException(ioe); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/ParameterSingleSelectionModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/ParameterSingleSelectionModel.java new file mode 100755 index 000000000..b9a37bc7d --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/ParameterSingleSelectionModel.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.util.Assert; + +/** + * An implementation of {@link SingleSelectionModel} that uses + * a state parameter for managing the currently selected key. + *

+ * + * A typical use case for this class is as follows. + *

public TheConstructor() {
+ *   m_parameter = new StringParameter("my_key");
+ *   m_sel = new ParameterSingleSelectionModel(m_parameter);
+ * }
+ *
+ * public void register(Page p) {
+ *   p.addComponent(this);
+ *   p.addComponentStateParam(this, m_param);
+ * }
+ * + * @author Stanislav Freidin + * @version $Id: ParameterSingleSelectionModel.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class ParameterSingleSelectionModel + extends AbstractSingleSelectionModel { + + + private ParameterModel m_parameter; + + /** + * Constructs a new ParameterSingleSelectionModel. + * + * @param m the parameter model that will be used to + * keep track of the currently selected key + */ + public ParameterSingleSelectionModel(ParameterModel m) { + super(); + + m_parameter = m; + } + + /** + * Returns the key that identifies the selected element. + * + * @param state a PageState value + * @return a String value. + */ + public Object getSelectedKey(PageState state) { + final FormModel model = state.getPage().getStateModel(); + if (model.containsFormParam(m_parameter)) { + return state.getValue(m_parameter); + } else { + return null; + } + } + + public final ParameterModel getStateParameter() { + return m_parameter; + } + + /** + * Set the selected key. + * + * @param state represents the state of the current request + * @param newKey the new selected key + */ + public void setSelectedKey(PageState state, Object newKey) { + final Object oldKey = getSelectedKey(state); + + if (Assert.isEnabled()) { + final FormModel model = state.getPage().getStateModel(); + Assert.isTrue(model.containsFormParam(m_parameter)); + } + + state.setValue(m_parameter, newKey); + + if (newKey == null && oldKey == null) { + return; + } + + if (newKey != null && newKey.equals(oldKey)) { + return; + } + + fireStateChanged(state); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/RequestLocal.java b/ccm-core/src/main/java/com/arsdigita/bebop/RequestLocal.java new file mode 100644 index 000000000..1bcace347 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/RequestLocal.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import java.util.HashMap; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; + +/** + * A variable whose value is local to each request. Objects that need to store + * values that change in every request should declare them to be + * RequestLocal. These variables hold their values only during a + * duration of a request. They get reinitialized by a call to {@link + * #initialValue(PageState)} for every new HTTP request. + * + *

For example, a class that wants to implement a request local property + * foo would do the following:

+ * + *
+ * public class SomeClass {
+ *     private RequestLocal m_foo;
+ *     
+ *     public SomeClass() {
+ *       m_foo = new RequestLocal() {
+ *             protected Object initialValue(PageState s) {
+ *                 // Foo could be a much more complicated value
+ *                 return s.getRequestURI();
+ *             }
+ *         };
+ *     }
+ *     
+ *     public String getFoo(PageState s) {
+ *         return (String) m_foo.get(s);
+ *     }
+ *     
+ *     public void setFoo(PageState s, String v) {
+ *         m_foo.set(s, v);
+ *     }
+ * }
+ * 
+ * + * @author David Lutterkort + * @version $Id$ + */ +public class RequestLocal { + + private static final String ATTRIBUTE_KEY = + "com.arsdigita.bebop.RequestLocal"; + + // Fetch the map used to store RequestLocals, possibly creating it along the + // way + private Map getMap(HttpServletRequest request) { + // This lock is paranoid. We can remove it if we know that only one + // thread will be touching a request object at a time. (Seems likely, + // but, like I said, I'm paranoid.) + synchronized (request) { + Map result = (Map)request.getAttribute(ATTRIBUTE_KEY); + result = (Map)request.getAttribute(ATTRIBUTE_KEY); + if (result == null) { + result = new HashMap(); + request.setAttribute(ATTRIBUTE_KEY, result); + } + return result; + } + } + + /** + * Returns the value to be used during the request represented by + * state. This method is called at most once per request, + * the first time the value of this RequestLocal is + * requested with {@link #get get}. RequestLocal must be + * subclassed, and this method must be overridden. Typically, an + * anonymous inner class will be used. + * + * + * @param state represents the current state of the request + * @return the initial value for this request local variable. + */ + protected Object initialValue(PageState state) { + return null; + } + + /** + * Returns the request-specific value for this variable for the request + * associated with state. + * + * @param state represents the current state of the request + * @return the value for this request local variable. + */ + public Object get(PageState state) { + Map map = getMap(state.getRequest()); + Object result = map.get(this); + + if ( result == null && !map.containsKey(this) ) { + result = initialValue(state); + set(state, result); + } + return result; + } + + /** + * Sets a new value for the request local variable and associates it with + * the request represented by state. + * + * @param state represents the current state of the request + * @param value the new value for this request local variable + */ + public void set(PageState state, Object value) { + set(state.getRequest(), value); + } + + /** + *

Sets a new value for the request local variable and associates it with + * the request represented by request

+ * + *

This method is intended for use when a Dispatcher needs to assign some + * value to a RequestLocal for Bebop Page processing before Page processing + * begins.

+ * + * @param request represents the current request + * @param value the new value for this request local variable + */ + public void set(HttpServletRequest request, Object value) { + getMap(request).put(this, value); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/SessionExpiredException.java b/ccm-core/src/main/java/com/arsdigita/bebop/SessionExpiredException.java new file mode 100755 index 000000000..9e0aad4ec --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/SessionExpiredException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import javax.servlet.ServletException; + +/** + * + * + * @version $Id: SessionExpiredException.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class SessionExpiredException extends ServletException { + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/SimpleComponent.java b/ccm-core/src/main/java/com/arsdigita/bebop/SimpleComponent.java new file mode 100755 index 000000000..06eef74e3 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/SimpleComponent.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import java.util.Collections; +import java.util.Iterator; + +import com.arsdigita.bebop.util.Attributes; +import com.arsdigita.kernel.KernelConfig; +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Element; + +/** + * A simple implementation of the Component interface. + * + * + * @author David Lutterkort + * @author Stanislav Freidin + * @author Rory Solomon + * @author Uday Mathur + * + * @version $Id: SimpleComponent.java 1498 2007-03-19 16:22:15Z apevec $ + */ +public class SimpleComponent extends Completable + implements Component, Cloneable { + + private boolean m_locked; + + /** + * The Attribute object is protected to make it easier for the Form Builder + * service to persist the SimpleComponent. + * Locking violation is not a problem since if the SimpleComponent is locked + * then the Attribute object will also be locked. + */ + protected Attributes m_attr; + + private String m_key = null; // name mangling key + + /** + * Clones a component. The clone is not locked and has its own set of + * attributes. + * @return the clone of a component. + * @throws java.lang.CloneNotSupportedException + * @post ! ((SimpleComponent) return).isLocked() + */ + @Override + public Object clone() throws CloneNotSupportedException { + SimpleComponent result = (SimpleComponent) super.clone(); + if ( m_attr != null ) { + result.m_attr = (Attributes) m_attr.clone(); + } + result.m_locked = false; + return result; + } + + /** + * Registers state parameters for the page with its model. Documentation + * from Interface Componment: + * + * A simple component with a state parameter param would do + * the following in the body of this method: + *
+     *   p.addComponent(this);
+     *   p.addComponentStateParam(this, param);
+     * 
+ * + * You should override this method to set the default visibility + * of your component: + * + *
+     * public void register(Page p) {
+     *     super.register(p);
+     *     p.setVisibleDefault(childNotInitiallyShown,false);
+     *     p.setVisibleDefault(anotherChild, false);
+     * }
+     * 
+ * + * Always call super.register when you override + * register. Otherwise your component may + * malfunction and produce errors like "Widget ... isn't + * associated with any Form" + * + * @pre p != null + * @param p + */ + @Override + public void register(Page p) { + return; + } + + /** + * Registers form parameters with the form model for this form. + * This method is only important for {@link FormSection form sections} + * and {@link com.arsdigita.bebop.form.Widget widgets} (components that + * have a connection to an HTML form). Other components can implement it + * as a no-op. + * + * @param f + * @param m + * @pre f != null + * @pre m != null + */ + @Override + public void register(Form f, FormModel m) { + return; + } + + /** + * Does processing that is special to the component + * receiving the click. + * + * @param state the current page state + * @throws javax.servlet.ServletException + */ + @Override + public void respond(PageState state) + throws javax.servlet.ServletException { } + + @Override + public Iterator children() { + return Collections.EMPTY_LIST.iterator(); + } + + /** Adds [J]DOM nodes for this component. Specifically for + * base class SimpleComponent, does nothing. + * @param p + */ + @Override + public void generateXML(PageState state, Element p) { + return; + } + + @Override + public final boolean isLocked() { + return m_locked; + } + + @Override + public void lock () { + if (m_attr != null) { + m_attr.lock(); + } + m_locked = true; + } + + /** + * Unlocks this component. Package visibility is intentional; the + * only time a component should be unlocked is when it's pooled and + * gets locked because it's put into a page. It needs to be unlocked + * when the instance is recycled. + */ + void unlock() { + m_locked = false; + + } + + /* Working with standard component attributes */ + + /** + * Gets the class attribute. + * @return the class attribute. + */ + @Override + public String getClassAttr() { + return getAttribute(CLASS); + } + + /** + * Sets the class attribute. + * @param theClass a valid XML name + */ + @Override + public void setClassAttr(String theClass) { + Assert.isUnlocked(this); + setAttribute(CLASS, theClass); + } + + /** + * Gets the style attribute. + * @return the style attribute. + */ + @Override + public String getStyleAttr() { + return getAttribute(STYLE); + } + + /** + * Sets the style attribute. style should be a valid CSS + * style, since its value will be copied verbatim to the output and + * appear as a style attribute in the top level XML or HTML + * output element. + * + * @param style a valid CSS style description for use in the + * style attribute of an HTML tag + * @see Standard Attributes + */ + @Override + public void setStyleAttr(String style) { + Assert.isUnlocked(this); + setAttribute(STYLE, style); + } + + /** + * Gets the id attribute. + * @return the id attribute. + * @see #setIdAttr(String id) + */ + @Override + public String getIdAttr() { + return getAttribute(ID); + } + + /** + * Sets the id attribute. id + * should be an XML name + * that is unique within the {@link Page Page} in which this component is + * contained. The value of id is copied literally to the + * output and not used for internal processing. + * + * @param id a valid XML identifier + * @see Standard Attributes + */ + @Override + public void setIdAttr(String id) { + Assert.isUnlocked(this); + setAttribute(ID, id); + } + + /* Methods for attribute management */ + + /** + * Sets an attribute. Overwrites any old values. These values are used to + * generate attributes for the top level XML or HTML element that is + * output from this component with {@link #generateXML generateXML}. + * + * @pre name != null + * @post getAttribute(name) == value + * + * @param name attribute name, case insensitive + * @param value new attribute value + */ + final protected void setAttribute(String name, String value) { + Assert.isUnlocked(this); + if (m_attr == null) { + m_attr = new Attributes(); + } + m_attr.setAttribute(name, value); + } + + /** + * Gets the value of an attribute. + * + * @pre name != null + * + * @param name attribute name, case insensitive + * @return the string value previously set with {@link #setAttribute + * setAttribute}, or null if none was set. + * @see #setAttribute + */ + final protected String getAttribute(String name) { + return (m_attr == null) ? null : m_attr.getAttribute(name); + } + + /** + * Adds the attributes set with {@link #setAttribute setAttribute} to the + * element target. The attributes set with + * exportAttributes overwrite attributes with identical names + * that target might already have. + * + * @pre target != null + * + * @param target element to which attributes are added + * @see #setAttribute + */ + final protected void exportAttributes(com.arsdigita.xml.Element target) { + if (m_attr != null) { + m_attr.exportAttributes(target); + } + if (KernelConfig.getConfig().isDebugEnabled() || + Bebop.getConfig().showClassName()) { + target.addAttribute("bebop:classname", getClass().getName(), + BEBOP_XML_NS); + } + } + + /** + * Returns true if any attributes have been set. + * @return true if any attributes have been set; + * false otherwise. + */ + final protected boolean hasAttributes() { + return m_attr != null; + } + + /* + * Set an arbitrary meta data attribute on the component. + * The name of the attribute in the XML will be prefixed + * with the string 'metadata.' + */ + final public void setMetaDataAttribute(String name, String value) { + setAttribute("metadata." + name, value); + } + + final public String getMetaDataAttribute(String name) { + return getAttribute("metadata." + name); + } + + /** + * Supplies a key for parameter name mangling. + * + * @param key the key to mangle + * @return + */ + @Override + public Component setKey(String key) { + Assert.isUnlocked(this); + if (key.charAt(0) >= 0 && key.charAt(0) <= 9) { + throw new IllegalArgumentException("key \"" + key + "\" must not start with a digit."); + } + m_key = key; + return this; + } + + /** + * Retrieves a key for parameter name mangling. + * @return a key for parameter name mangling. + */ + @Override + public final String getKey() { + return m_key; + } + + @Override + public boolean isVisible(PageState s) { + return s.isVisible(this); + } + + @Override + public void setVisible(PageState s, boolean v) { + s.setVisible(this, v); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/SimpleContainer.java b/ccm-core/src/main/java/com/arsdigita/bebop/SimpleContainer.java new file mode 100755 index 000000000..aa2278bc3 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/SimpleContainer.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Element; + +/** + * A basic implementation of the {@link Container} interface which, by default, + * renders all of its children directly, without wrapping them in any kind of + * tag. + * + * However, the {@link #SimpleContainer(String, String)} constructor and/or the + * {@link #setTag(String)} method can be used to cause the container to wrap + * the XML for its children in an arbitrary tag. This functionality is useful + * for XSL templating. + * + * For example, a template rule might be written to arrange the children of this + * component in paragraphs: + * + *

+ * // Java Code:
+ * m_container = new SimpleContainer("cms:foo", CMS_XML_NS);
+ *
+ * // XSL code:
+ * <xsl:template match="cms:foo">
+ *   <xsl:for-each select="*">
+ *     <p>
+ *     <xsl:apply-templates select="."/>
+ *     </p>
+ *   </xsl:for-each>
+ * </xsl:template>
+ * 
+ * + * @author David Lutterkort + * @author Stanislav Freidin + * @author Rory Solomon + * @author Uday Mathur + * + * @version $Id: SimpleContainer.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class SimpleContainer extends SimpleComponent implements Container { + + private List m_components; + private String m_tag, m_ns; + + /** + * Constructs a new, empty SimpleContainer. + */ + public SimpleContainer() { + this(null, null); + } + + /** + * Constructs a new, empty SimpleContainer that will + * wrap its children in the specified tag. + * + * @param tag the name of the XML element that will be used to wrap the + * children of this container + * @param ns the namespace for the tag + */ + public SimpleContainer(String tag, String ns) { + super(); + m_components = new ArrayList(); + m_tag = tag; + m_ns = ns; + } + + /** + * Adds a component to this container. + * + * @param pc the component to be added + */ + public void add(Component pc) { + Assert.isUnlocked(this); + Assert.exists(pc); + m_components.add(pc); + } + + /** + * Adds a component to this container. + * + * @param pc the component to be added + * @param constraints this parameter is ignored. Child classes should + * override the add method if they wish to provide special handling + * of constraints. + */ + public void add(Component c, int constraints) { + add(c); + } + + /** + * Determines membership. + * @return true if the specified object is in this container; + * false otherwise. + * @param o the object type, typically a component. Type + * Object allows slicker code when o comes from any kind of collection. + */ + public boolean contains(Object o) { + return m_components.contains(o); + } + + /** + * Determines whether the container is empty. + * + * @return false if the container has any children; + * true otherwise. + */ + public boolean isEmpty() { + return m_components.isEmpty(); + } + + /** + * + * + * + */ + public int indexOf(Component pc) { + return m_components.indexOf(pc); + } + + /** + * Returns the number of children inside this container. + * @return the number of children inside this container. + */ + public int size() { + return m_components.size(); + } + + /** + * + * + * + */ + public Component get(int index) { + return (Component) m_components.get(index); + } + + /** + * Returns all the components of this container. + * @return all the components of this container. + */ + @Override + public Iterator children() { + return m_components.iterator(); + } + + /** + * Sets the XML tag that will be used to wrap the children of + * this container. + * + * @param tag the XML tag, or null if children will not be wrapped + * in any manner. + */ + protected final void setTag(String tag) { + Assert.isUnlocked(this); + m_tag = tag; + } + + /** + * Sets the XML namespace for the tag that will be used to wrap + * the children of this container. + * + * @param ns the XML namespace + */ + protected final void setNamespace(String ns) { + Assert.isUnlocked(this); + m_ns = ns; + } + + /** + * Retrieves the name of the XML tag that will be used to + * wrap the child components. + * + * @return the name of the XML tag that will be used to + * wrap the child components, or null if no tag was specified. + */ + public final String getTag() { + return m_tag; + } + + /** + * Retrieves the name of the XML namespace for the tag that will be used to + * wrap the child components. + * + * @return the name of the XML namespace for the tag that will be used to + * wrap the child components, or null if no namespace was specified. + */ + public final String getNamespace() { + return m_ns; + } + + /** + * Generates the containing element. It is added with this + * component's tag below the specified parent element. If the passed in + * element is null, the method + * passes through p. + + * @param p the parent XML element + * @return the element to which the children will be added. + */ + protected Element generateParent(Element p) { + String tag = getTag(); + if (tag == null) { + return p; + } + Element parent = p.newChildElement(tag, getNamespace()); + exportAttributes(parent); + return parent; + } + + /** + * Generates the XML for this container. If the tag property + * is nonempty, wraps the children in the specified XML tag. + * + * @param state represents the current request + * @param p the parent XML element + * @see #setTag(String) + * @see #setNamespace(String) + */ + public void generateChildrenXML(PageState state, Element p) { + for (Iterator i = children(); i.hasNext(); ) { + Component c = (Component) i.next(); + + // XXX this seems to be a redundant vis check + if ( c.isVisible(state) ) { + c.generateXML(state, p); + } + } + } + + /** + * Generates the XML for this container. If the tag property + * is nonempty, wraps the children in the specified XML tag. + * + * @param state represents the current request + * @param p the parent XML element + * @see #setTag(String) + * @see #setNamespace(String) + */ + @Override + public void generateXML(PageState state, Element p) { + if ( isVisible(state) ) { + Element parent = generateParent(p); + generateChildrenXML(state, parent); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/SingleSelectionModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/SingleSelectionModel.java new file mode 100755 index 000000000..02d1f3daf --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/SingleSelectionModel.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + + +import com.arsdigita.bebop.event.ChangeListener; +import com.arsdigita.bebop.parameters.ParameterModel; + +/** + * Encapsulates the selection of a single object from many + * possibilities. The SingleSelectionModel allows components to + * communicate selections without tying the component that manages the + * selection in the user interface (for example a {@link List}) to the + * components that consume the selection (such as an edit form that needs + * to know which object should be edited). + * + *

Selections are identified by a key, which must identify the + * underlying object uniquely among all objects that could possibly be + * selected. For objects stored in a database, this is usually a suitable + * representation of the object's primary key. The model relies on the + * key's equals method to compare keys, and requires that the + * key's toString method produces a representation of the key + * that can be used in URL strings and hidden form controls. + * + * @author David Lutterkort + * @version $Id: SingleSelectionModel.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public interface SingleSelectionModel { + + /** + * Returns true if there is a selected element. + * + * @param state the state of the current request + * @return true if there is a selected component; + * false otherwise. + */ + boolean isSelected(PageState state); + + /** + * Returns the key that identifies the selected element. + * + * @param state a PageState value + * @return a String value. + */ + Object getSelectedKey(PageState state); + + /** + * Sets the selected key. If key is not in the collection of + * objects underlying this model, an + * IllegalArgumentException is thrown. + * + * @param state the state of the current request + * @param key the selected key + * @throws IllegalArgumentException if the supplied key can not + * be selected in the context of the current request. + */ + void setSelectedKey(PageState state, Object key); + + /** + * Clears the selection. + * + * @param state the state of the current request + * @post ! isSelected(state) + */ + void clearSelection(PageState state); + + /** + * Adds a change listener to the model. The listener's + * stateChanged method is called whenever the selected key changes. + * + * @param l a listener to notify when the selected key changes + */ + void addChangeListener(ChangeListener l); + + /** + * Removes a change listener from the model. + * + * @param l the listener to remove + */ + void removeChangeListener(ChangeListener l); + + /** + * Returns the state parameter that will be used to keep track + * of the currently selected key. Typically, the implementing + * class will simply call:
+ *

return new StringParameter("foo");

+ * This method may return null if a state parameter is not + * appropriate in the context of the implementing class. + * + * @return the state parameter to use to keep + * track of the currently selected component, or + * null if a state parameter is not appropriate. + */ + ParameterModel getStateParameter(); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionEvent.java new file mode 100755 index 000000000..fba6ef94c --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.PageState; + +/** + * A component-defined event. Components usually fire an + * ActionEvent to indicate that they were the ones receiving + * the click in a user's submission, for example, a form might use an + * ActionEvent to signal that it has been submitted. + * + * @see ActionListener + * @see java.awt.event.ActionEvent + * + * @author David Lutterkort + * + * @version $Id$ + */ + +public class ActionEvent extends PageEvent { + + /** + * Construct an ActionEvent. + * + * @param source the component that originated the event + * @param state the state of the containing page under the current + * request + */ + public ActionEvent(Component source, PageState state) { + super(source, state); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionListener.java new file mode 100755 index 000000000..a1d7674da --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionListener.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import java.util.EventListener; + +/** + * The listener interface for receiving action events. The class that is + * interested in processing an action event implements this interface, and + * the object created with that class is registered with a component, using + * the component's addActionListener method. When the action event occurs, + * that object's actionPerformed method is invoked. + * + * @see ActionEvent + * @see java.awt.event.ActionListener + * + * @author David Lutterkort + * + * @version $Id$ + */ +public interface ActionListener extends EventListener { + + /** + * Invoked when an action has been performed. + * + * @pre e != null + */ + void actionPerformed(ActionEvent e); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeEvent.java new file mode 100755 index 000000000..8876f1915 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeEvent.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.PageState; + +/** + * This class will be + * renamed to SelectionEvent. + */ +public class ChangeEvent extends PageEvent { + + public ChangeEvent(Object source, PageState state) { + super(source, state); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeListener.java new file mode 100755 index 000000000..f69dfc1dd --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeListener.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import java.util.EventListener; + +/** + * This class will be + * renamed to SelectionListener. + */ +public interface ChangeListener extends EventListener { + + void stateChanged(ChangeEvent e); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/EventListenerList.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/EventListenerList.java new file mode 100755 index 000000000..8125c5d2f --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/EventListenerList.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Convenience extensions to {@link javax.swing.event.EventListenerList + * Swing's EventListenerList}. + * @version $Id$ + */ +public class EventListenerList extends javax.swing.event.EventListenerList { + + /** + * Append all the event listeners from l. + * + * @param l The list of listeners to copy from + * + * @pre l != null + */ + public void addAll(EventListenerList l) { + + if ( l.listenerList.length == 0 ) + return; + + Object[] tmp = new Object[listenerList.length + l.listenerList.length]; + System.arraycopy(listenerList, 0, tmp, 0, listenerList.length); + System.arraycopy(l.listenerList, 0, + tmp, listenerList.length, l.listenerList.length); + listenerList = tmp; + } + + /** + * Return an iterator over all event listeners of class t. + * This iterator replaces the for loop mentioned in the documentation for + * {@link javax.swing.event.EventListenerList Swing's + * EventListenerList}. + * + * @param t The class of the event listeners that should be returned + * + * @pre t != null + * */ + public Iterator getListenerIterator(final Class t) { + return new EventListenerIterator(t); + } + + private class EventListenerIterator implements Iterator { + + /** + * The listener we will return with the next call to next(). + * listener[_next] is always a class object of type t, unless all + * matching listeners have been returned, in which case _next + * is -1 + * */ + private int _count; + private int _next; + private Class _t; + + EventListenerIterator(Class t) { + + _count = getListenerList().length; + _next = -2; + _t = t; + findNext(); + } + + public boolean hasNext() { + return (_next < _count); + } + + public Object next() throws NoSuchElementException { + if ( ! hasNext() ) { + throw new NoSuchElementException("Iterator exhausted"); + } + int result = _next; + findNext(); + return getListenerList()[result+1]; + } + + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Removal not supported"); + } + + /** + * Advance _next so that either _next == -1 + * if all listeners of class _t have been returned in the + * enclosing EventListenerList, or that + * getListenersList()[_next] == _t and + * getListenersList()[_next+1] (the corresponding listener + * object) has not been returned yet by next(). + * */ + private void findNext() { + + for (int i = _next+2; i<_count; i+=2) { + + if (getListenerList()[i] == _t) { + _next = i; + return; + } + } + _next = _count; + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormCancelListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormCancelListener.java new file mode 100755 index 000000000..7b2a14c92 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormCancelListener.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.FormProcessException; +import java.util.EventListener; + +/** + * Defines the interface for a class that performs cleanup after + * cancelling out of a form + * + * @author Kevin Scaldeferri + * @version $Id$ + */ + +public interface FormCancelListener extends EventListener { + + /** + * Performs any necessary cleanup after a user cancels out of + * a form + * + *

Implementations of this method are responsible for catching + * specific exceptions that may occur during processing, and either + * handling them internally or rethrowing them as instances of + * FormProcessException to be handled by the calling + * procedure. + */ + + void cancel(FormSectionEvent e) throws FormProcessException; + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormInitListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormInitListener.java new file mode 100755 index 000000000..08e964c19 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormInitListener.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.FormProcessException; +import java.util.EventListener; + +/** + * Defines the interface for initializing a form with default values. + * Typical implementations of this interface query the database to + * set up an "edit" form, or obtain an id from a sequence to initialize + * a "create" form. + * + * @author Karl Goldstein + * @author Uday Mathur + * @version $Id$ + */ +public interface FormInitListener extends EventListener { + + /** + * Initializes a FormData object already populated with values from + * the request. + * + * @param date The form data containing data included with this + * request. The initializer may require knowledge of form or + * parameter properties. + * + * @param request The HTTP request associated with the + * initialization event. This supplied so that the initializer may + * rely on contextual information, such information extracted from + * headers or cookies or an associated HttpSession + * object. + * */ + void init(FormSectionEvent e) throws FormProcessException; + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormProcessListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormProcessListener.java new file mode 100755 index 000000000..a3abaf87a --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormProcessListener.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.FormProcessException; +import java.util.EventListener; + +/** + * Defines the interface for a class that performs a processing step + * on valid data. + * + * @author Karl Goldstein + * @author Uday Mathur + * @version $Id$ + */ + +public interface FormProcessListener extends EventListener { + + /** + * Performs a processing step on the data in the + * FormData object. + * + *

Implementations of this method are responsible for catching + * specific exceptions that may occur during processing, and either + * handling them internally or rethrowing them as instances of + * FormProcessException to be handled by the calling + * procedure. + * + *

Implementations of this method cannot assume success or + * failure of other FormProcessListeners associated with a + * particular FormModeel. Each implementation must act independently + * + * @param model The form model describing the structure and properties + * of the form data included with this request. + * + * @param data The container for all data objects associated with + * the request. String values for all parameters specified in the + * form model are converted to Java data objects and validated + * before processing occurs. + * + * @param request The HTTP request information from which the form + * data was extracted. Note that the request object is supplied + * only in case the processing step requires contextual information + * (information extracted from cookies or the peer address, for + * example) or needs to modify session properties. + * + * @param response The HTTP response that will be returned to the + * user. The processing step may require access to this object to + * set cookies or handle errors. */ + + void process(FormSectionEvent e) throws FormProcessException; + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSectionEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSectionEvent.java new file mode 100755 index 000000000..70996cc30 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSectionEvent.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.FormData; +import com.arsdigita.bebop.PageState; + +/** + * An event originating from a form. FormSectionEvents are + * used to notify listeners that values in a form should be initilialized, + * validated or processed. + * + * @author David Lutterkort + * + * @version $Id$ + * + * @see FormInitListener + * @see FormValidationListener + * @see FormProcessListener + */ +public class FormSectionEvent extends PageEvent { + + private final transient FormData _formData; + + /** + * Get the form data for to the form that fired the event in the current + * request. + * + * @return form data + */ + public final FormData getFormData() { + return _formData; + } + + /** + * Construct a FormSectionEvent. + * + * @param source the form model that fired the event + * @param state the state of the enclosing page + * @param formData the form data constructed so far + */ + public FormSectionEvent(Object source, + PageState state, + FormData formData) { + super(source, state); + _formData = formData; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSubmissionListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSubmissionListener.java new file mode 100755 index 000000000..127c25c33 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSubmissionListener.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import java.util.EventListener; + +import com.arsdigita.bebop.FormProcessException; + +/** + * The listener called just before a form starts examining a + * submission. This listener can throw a {@link FormProcessException} to + * indicate that any further processing of the submission should be + * aborted. This usually leaves the corresponding {@link + * com.arsdigita.bebop.FormData FormData} object in an undefined + * state. + * + * @author David Lutterkort + * @version $Id$ + */ +public interface FormSubmissionListener extends EventListener { + + /** + * This method gets called as soon as the FormData for a + * form has been filled with the request parameters. The values in the + * FormData are transformed but not validated. + * + * @param e the event encapsulating form data, page state and event source + * @throws FormProcessException to signal that further processing of the + * form should be aborted. + */ + void submitted(FormSectionEvent e) throws FormProcessException; + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormValidationListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormValidationListener.java new file mode 100755 index 000000000..29ff676c4 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormValidationListener.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.FormProcessException; +import java.util.EventListener; + +/** + * Defines the interface for a class that implements a validation check + * on a set of form data. + * + * @author Karl Goldstein + * @author Uday Mathur + * @version $Id$ + */ +public interface FormValidationListener extends EventListener { + + /** + * Performs a validation check on the specified FormData + * object, involving any number of parameters. + * + *

The check is always performed after all HTTP request + * parameters have been converted to data objects and stored in the + * FormData object. + * + *

If a validation error is encountered, the setError + * method of the FormData object may be used to set an + * error message for reporting back to the user. + * + *

This method is responsible for catching any exceptions that + * may occur during the validation. These exceptions may either + * be handled internally, or if they are unrecoverable may be + * rethrown as instances of FormProcessException. + * + * @param e FormSectionEvent containing the FormData as well as the + * PageState. + * Clients may access the PageState by executing something like + * PageState state = fse.getPageState(); + * Method getFormData() allows access to the Form's data. + * + * @exception FormProcessException ff the data does not pass the check. + */ + + void validate(FormSectionEvent e) throws FormProcessException; + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/PageEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/PageEvent.java new file mode 100755 index 000000000..8491849f3 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/PageEvent.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.PageState; + +import java.util.EventObject; + +/** + * The base class for all page related events. All page related events + * should be derived from this class, since it defines a standard way to + * get at the source of the event and at the state of the page under the + * request that is currently being processed. + * + * @author David Lutterkort + * + * @version $Id$ + */ +public class PageEvent extends EventObject { + + private transient PageState _state; + + /** + * Construct a new PageEvent. + * @param source the object firing the event, usually a {@link + * com.arsdigita.bebop.Component Component}. + * @param state the state of the page under the current request + */ + public PageEvent(Object source, PageState state) { + super(source); + _state = state; + } + + /** + * Get the state of the page under the request in which the event was fired + */ + public final PageState getPageState() { + return _state; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterEvent.java new file mode 100755 index 000000000..96696577d --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterEvent.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.parameters.ParameterData; + +import java.util.EventObject; + +/** + * An event connected to a request parameter. + * + * @author David Lutterkort + * + * @version $Id$ + * + * @see ParameterListener + * @see com.arsdigita.bebop.parameters.ParameterModel + * @see com.arsdigita.bebop.parameters.ParameterData + */ + +public class ParameterEvent extends EventObject { + + /* The request specific data about the event */ + private transient ParameterData m_data; + private transient PageState m_state; + + /** + * Construct a ParameterEvent + * + * @param source the object that originated the event + * @param data the data for the parameter from the current request + **/ + + public ParameterEvent(Object source, ParameterData data) { + super(source); + m_data = data; + m_state = PageState.getPageState(); + } + + + /** + * Get the request specific data about the parameter. + **/ + + public final ParameterData getParameterData() { + return m_data; + } + + + /** + * + **/ + + public PageState getPageState() { + return m_state; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterListener.java new file mode 100755 index 000000000..dd1a2499e --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterListener.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.FormProcessException; +import java.util.EventListener; + +/** + * Defines the interface for a class that validates the values of a + * single parameter. + * + * @author Karl Goldstein + * @author Uday Mathur + * @version $Id$ */ + +public interface ParameterListener extends EventListener { + + /** + * Performs a validation check on the data objects associated with a + * specific parameter. Validate should call + * ParameterData.addError() with a message regarding the nature + * of the error. + * @param e + * @throws com.arsdigita.bebop.FormProcessException + */ + void validate(ParameterEvent e) throws FormProcessException; +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintEvent.java new file mode 100755 index 000000000..7b22af400 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintEvent.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.PageState; + +/** + * An event originating from a component. PrintEvents are + * fired just before the source component is output either as + * part of an XML document or as part of an HTML page. + * + * @see PrintListener + * + * @author Uday Mathur + * @author David Lutterkort + * + * @version $Id$ + * + */ +public class PrintEvent extends PageEvent { + + private Object m_target; + + /** + * Construct a PrintEvent + * + * @param source the object that originated the event + * @param data the data for the parameter from the current request + * @pre source != null + * @pre target != null + */ + public PrintEvent(Object source, PageState state, Object target) { + super(source, state); + m_target = target; + } + + /** + * Get the target object, the one that can be freely modified by print + * listeners. Initially, the target is an unlocked clone of the source of + * the event. + * @post return != null + */ + public final Object getTarget() { + return m_target; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintListener.java new file mode 100755 index 000000000..0613f31ba --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintListener.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import java.util.EventListener; + +/** + * Listeners of this class are called just before a {@link com.arsdigita.bebop.Component} is + * about to be output, either in the form of an XML element, or by printing + * its HTML representation. The {@link #prepare prepare method} of the + * listener can make modifications to the {@link PrintEvent#getTarget + * target} of the event. The target will then be used to produce output + * instead of the source. + *

+ * {@link PrintEvent PrintEvents} are unicast events, which means + * that components should only permit the registration of one + * PrintListener. Since the PrintListener is + * expected to modify the target, allowing multiple listeners to modify the + * target of one event would make it impossible to predict the resulting + * target component, since an individual listener can not know which + * listeners have run before it and which ones will run after it. + *

+ * As an example consider the following code: + *

+ *   Label l = new Label("Default text");
+ *   l.addPrintListener( new PrintListener {
+ *     private static final BigDecimal ONE = new BigDecimal(1);
+ *     private BigDecimal count = new BigDecimal(0);
+ *     public void prepare(PrintEvent e) {
+ *       Label t = e.getTarget();
+ *       synchronized (count) {
+ *         count.add(ONE);
+ *       }
+ *       t.setLabel("Call no." + count + " since last server restart");
+ *     }
+ *   });
+ * Adding the label l to a page will lead to a label that + * changes in every request and print how many times the containing label + * has been called. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author David Lutterkort + * @version $Id$ + */ + +public interface PrintListener extends EventListener { + + /** + * Prepare the target component returned by {@link PrintEvent#getTarget + * e.getTarget()} for output. The target component is an unlocked clone + * of the source of the event and can be freely modified within this + * method. + * + * @param e Event containing the page state, the source and the target of + * the event + * + * @see PrintEvent + */ + + void prepare(PrintEvent e); + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestEvent.java new file mode 100755 index 000000000..f2017c112 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestEvent.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.PageState; + +/** + * An event indicating that a Bebop page is being loaded, and control + * is about to be passed to the currently selected component + * + * @author David Lutterkort + * + * @version $Id$ + * + * @see ActionListener + * @see java.awt.event.ActionEvent + */ + +public class RequestEvent extends PageEvent { + + /** + * Construct an ActionEvent. + * + * @param source the component that originated the event + * @param state the state of the containing page under the current + * request + */ + public RequestEvent(Component source, PageState state) { + super(source, state); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestListener.java new file mode 100755 index 000000000..3219cf2e8 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestListener.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import java.util.EventListener; + +/** + * The listener interface for receiving request events. The class that is + * interested in processing a request event implements this interface, and + * the object created with that class is registered with a Bebop page, using + * the Page.addRequestListener method. When the page has finished processing + * the page state, and is about to pass control to the currently selected + * component, the pageRequested method will be called. + * + * @author David Lutterkort + * + * @version $Id$ + * + * @see ActionEvent + * @see java.awt.event.ActionListener + */ +public interface RequestListener extends EventListener { + + /** + * Invoked when an action has been performed. + * + * @pre e != null + */ + void pageRequested(RequestEvent e); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectListener.java new file mode 100755 index 000000000..1f626414a --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectListener.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import java.util.EventListener; + +/** + * Analogous to Widget + * PrintListeners, this is called when the widget is displayed (or + * validated) to get the dataset. The dataset should be created + * dynamically so it can vary according to form variables. + * Eventually, this may also support setting the initial value for a + * SearchAndSelect widget, so that it may act as an edit widget as + * well. + * + * @author Patrick McNeill + * @version $Id$ + * @since 4.5 */ +public interface SearchAndSelectListener extends EventListener { + + SearchAndSelectModel getModel( PageEvent e ); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectModel.java new file mode 100755 index 000000000..a8585fa10 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectModel.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +/** + * Listener interface for + * the SeachAndSelect Bebop widget. SearchAndSelect requires + * knowledge about the data it is searching over (to determine the + * display method and to actually execute the query). + * + * @author Patrick McNeill + * @version $Id$ + * @since 4.5 */ +public interface SearchAndSelectModel { + + /** + * Specify the user's search and restrict the result set to those queries + * that match. An empty string should return all results. + * + * @param query the user's search string, space or comma delimited words + */ + void setQuery ( String query ); + + /** + * Retrieve the query that was last used. + * + * @return the query string + */ + String getQuery (); + + /** + * Return the number of items that are currently selected by the query + * string. If the query string is empty, this should return the number + * of items in the dataset. + * + * @return the number of currently selected items + */ + int resultsCount (); + + /** + * Get the "i"th label (0 based indexing) + * + * @param i the label number to retrieve + * @return the ith label + */ + String getLabel (int i); + + /** + * Get the "i"th ID (0 based indexing) + * + * @param i the ID number to retrieve + * @return the ith ID + */ + String getID (int i); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionAdapter.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionAdapter.java new file mode 100755 index 000000000..d9225ffef --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionAdapter.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +/** + * An implentation of the TableActionListener interface meant to save the + * developer from having to override both the {@link + * #cellSelected(TableActionEvent)} and {@link #headSelected(TableActionEvent)} + * methods when they only need to change the behavior of one. + * + * @see TableActionEvent + * @author David Lutterkort + * @version $Id$ + */ +public class TableActionAdapter implements TableActionListener { + + /** + * A no-op implementation of {@link + * TableActionListener#cellSelected(TableActionEvent)}. + * + * @param e the event fired for the table. + */ + public void cellSelected(TableActionEvent e) { + return; + } + + /** + * A no-op implementation of {@link + * TableActionListener#headSelected(TableActionEvent)}. + * + * @param e the event fired for the table. + */ + public void headSelected(TableActionEvent e) { + return; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionEvent.java new file mode 100755 index 000000000..f141f31a0 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionEvent.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.PageState; + +/** + * An event for the {@link com.arsdigita.bebop.Table} component. + * Table will fire this event when one of its active cells receives a + * click. + * + * @see TableActionListener + * @see TableActionAdapter + * @author David Lutterkort + * @version $Id$ + */ +public class TableActionEvent extends ActionEvent { + + private Object m_rowKey; + private Integer m_column; + + /** + * Construct a TableActionEvent for a click on a particular row + * and a particular column. + * + * @param source the Component generating the event. + * @param s the state for the current request. + * @param rowKey the key for the row where the click was registered. + * @param column the index of the column where the click was registered. + */ + public TableActionEvent(Component source, PageState s, + Object rowKey, Integer column) { + super(source, s); + m_rowKey = rowKey; + m_column = column; + } + + /** + * Construct a TableActionEvent for a click on a particular row. + * + * @param source the Component generating the event. + * @param s the state for the current request. + * @param rowKey the key for the row where the click was registered. + */ + public TableActionEvent(Component source, PageState s, Object rowKey) { + this(source, s, rowKey, new Integer(-1)); + } + + /** + * Get the key for the row that received the click. + * + * @return the key for the row that received the click. + */ + public final Object getRowKey() { + return m_rowKey; + } + + /** + * Get the index of the column that received the click. + * + * @return the index of the column that received the click. + */ + public final Integer getColumn() { + return m_column; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionListener.java new file mode 100755 index 000000000..2e6cbd4a4 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionListener.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import java.util.EventListener; + +/** + * Specifies the interface for handling events on {@link + * com.arsdigita.bebop.Table}. Programmers wishing to override just + * one of these methods, not both, may prefer to use {@link + * TableActionAdapter}. + * + * @see TableActionEvent + * @see TableActionAdapter + * @author David Lutterkort + * @version $Id$ + */ +public interface TableActionListener extends EventListener { + + /** + * An event handler for actions on a particular cell or a set of + * cells. + * + * @param e the event fired for the table. + */ + void cellSelected(TableActionEvent e); + + /** + * An event handler for actions on a particular column heading or + * set of column headings. + * + * @param e the event fired for the table. + */ + void headSelected(TableActionEvent e); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionEvent.java new file mode 100755 index 000000000..db4f05586 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionEvent.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.PageState; + +/** + * An event for the {@link com.arsdigita.bebop.Tree} component. + * Tree will fire this event when one of its nodes is expanded or + * collapsed. + * + * @author David Lutterkort + * @version $Id$ + */ +public class TreeExpansionEvent extends ActionEvent { + + private Object m_nodeKey; + + public TreeExpansionEvent(Component source, PageState s, Object nodeKey) { + super(source, s); + m_nodeKey = nodeKey; + } + + /** + * Get the key for the node that was expanded or collapsed. + * + * @return the key for the node that was expanded or collapsed. + */ + public final Object getNodeKey() { + return m_nodeKey; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionListener.java new file mode 100755 index 000000000..a5c8e12cd --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionListener.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.event; + +import java.util.EventListener; + +/** + * The listener that is notified when a tree node is expanded or + * collapsed. + * + * @author David Lutterkort + * @version $Id$ + */ +public interface TreeExpansionListener extends EventListener { + + /** + * Called whenever an item in the tree has been collapsed. + */ + void treeCollapsed(TreeExpansionEvent event); + + /** + * Called whenever an item in the tree has been expanded. + */ + void treeExpanded(TreeExpansionEvent event); + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/CheckboxGroup.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/CheckboxGroup.java new file mode 100755 index 000000000..38c3adb69 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/CheckboxGroup.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + + + + +import com.arsdigita.bebop.parameters.ArrayParameter; +// This interface contains the XML element name of this class +// in a constant which is used when generating XML +import com.arsdigita.bebop.util.BebopConstants; + +/** + * A class representing a group of associated checkboxes. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Rory Solomon + * @author Michael Pih + * @version $Id$ + */ +public class CheckboxGroup extends OptionGroup implements BebopConstants { + + public CheckboxGroup(String name) { + this(new ArrayParameter(name)); + } + + public CheckboxGroup(ArrayParameter param) { + super(param); + //m_xmlElement = BEBOP_CHECKBOX; + } + + /** + * Returns a string naming the type of this widget. + */ + public String getType() { + return "checkbox"; + } + + /** The XML tag. + * @return The tag to be used for the top level DOM element + * generated for this type of Widget. */ + @Override + protected String getElementTag() { + return BEBOP_CHECKBOXGROUP; + } + + @Override + public String getOptionXMLElement() { + return BEBOP_CHECKBOX; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/DHTMLEditor.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/DHTMLEditor.java new file mode 100755 index 000000000..84bbe2eb5 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/DHTMLEditor.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2001-2006 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.arsdigita.bebop.Bebop; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.bebop.parameters.StringParameter; +import com.arsdigita.web.Web; +import com.arsdigita.xml.Element; + +/** + * Displays and manages a WYSIWYG HTML editor that takes advantage of DHTML scripting features. This + * class can use: - FCKeditor + * - HTMLarea for backwards compatibility, development discontinued Editor is choosen based on the + * config parameter waf.bebop.dhtml_editor, default is "Xinha", which is the successor of HTMLarea + * + * @author Jim Parsons + * @author Richard Li + * @author Chris Burnett + * @author Alan Pevec + * + * @version $Id$ + */ +public class DHTMLEditor extends TextArea { + + /** + * Constant for specifying OFF value for the WRAP attribute of this image + * input. + * + * See here + * for a description of what this attribute does. + */ + public static final int OFF = 0; + + /** + * Constant for specifying HARD value for the WRAP attribute of this image + * input. + * + * See here + * for a description of what this attribute does. + */ + public static final int HARD = 1; + + /** + * Constant for specifying SOFT value for the WRAP attribute of this image + * input. See here + * for a description of what this attribute does. + */ + public static final int SOFT = 2; + + /** + * Config objects for supported DHTMP editors + */ + public static class Config { + + // WARNING: Processing of these default values by CMSConfig does NOT + // work correctly because of deviciencies in unmarshal method there. + public static final Config STANDARD = new Config("Xinha.Config", + "/assets/xinha/CCMcoreXinhaConfig.js"); + + /** + * Example FCKEditor configuration. + */ + public static final Config FCK_STANDARD = new Config("FCKEditor.Config.StyleDefault", + "/assets/fckeditor/config/fckconfigstyledefault.js"); + + public static final Config FCK_CMSADMIN = new Config("FCKEditor.Config.StyleCMSAdmin", + "/assets/fckeditor/config/fckconfigstylecmsadmin.js"); + + /** + * Example old HTMLarea configuration. + */ + public static final Config HTMLAREA = new Config("HTMLArea.Config", null); + + private String m_name; + private String m_path; + + public Config(String name) { + this(name, null); + } + + public Config(String name, + String path) { + m_name = name; + m_path = path; + } + + public String getName() { + return m_name; + } + + public String getPath() { + return m_path; + } + + public static Config valueOf(String cfg) { + int offset = cfg.indexOf(","); + if (offset != -1) { + return new Config(cfg.substring(0, offset), + cfg.substring(offset + 1)); + } else { + return new Config(cfg); + } + } + + public String toString() { + if (m_path == null) { + return m_name; + } else { + return m_name + "," + m_path; + } + } + + } //end config object(s) + + private Config m_config; + private Set m_plugins; + private Set m_hiddenButtons; + + /** + * Constructor + * + * @param name + */ + public DHTMLEditor(String name) { + this(new StringParameter(name)); + } + + /** + * Constructor + * + * @param model + */ + public DHTMLEditor(ParameterModel model) { + this(model, Config.STANDARD); + } + + /** + * Constructor + * + * @param model + * @param config + */ + public DHTMLEditor(ParameterModel model, + Config config) { + super(model); + m_config = config; + m_plugins = new HashSet(); + m_hiddenButtons = new HashSet(); + } + + /** + * Returns a string naming the type of this widget. + */ + public String getType() { + return "DHTMLEditor"; + } + + public String getEditorURL() { + return Bebop.getConfig().getDHTMLEditorSrcFile().substring( + 0, Bebop.getConfig().getDHTMLEditorSrcFile().lastIndexOf("/") + 1); + } + + public String getEditorSrc() { + return Bebop.getConfig().getDHTMLEditorSrcFile(); + } + + /** + * deprecated - use {@link setConfig(Config)} + * + * @param config + */ + public void setConfig(String config) { + setAttribute("config", config); + } + + public void setConfig(Config config) { + m_config = config; + } + + public void addPlugin(String name) { + m_plugins.add(name); + } + + /** + * Prevent the specified button from being displayed in the editor toolbar. + * + * @param name name of the button, as specified in the btnList of the htmlarea.js file + * + */ + public void hideButton(String name) { + m_hiddenButtons.add(name); + } + + /** + * Sets the ROWS attribute for the TEXTAREA tag. + */ + @Override + public void setRows(int rows) { + setAttribute("rows", String.valueOf(rows)); + } + + /** + * Sets the COLS attribute for the TEXTAREA tag. + */ + @Override + public void setCols(int cols) { + setAttribute("cols", String.valueOf(cols)); + } + + /** + * Sets the COLS attribute for the TEXTAREA tag. + */ + @Override + public void setWrap(int wrap) { + String wrapString = null; + + switch (wrap) { + case OFF: + wrapString = "off"; + break; + case HARD: + wrapString = "hard"; + break; + case SOFT: + wrapString = "soft"; + break; + } + + if (wrapString != null) { + setAttribute("wrap", wrapString); + } + } + + /** + * The XML tag. + * + * @return The tag to be used for the top level DOM element generated for this type of Widget. + */ + @Override + protected String getElementTag() { + return Bebop.getConfig().getDHTMLEditor(); + } + + /** + * Generates the DOM for the DHTML editor widget + *

+ * Generates DOM fragment: + *

+ * <bebop:dhtmleditor name=... value=... [onXXX=...]/> + * + */ + @Override + public void generateWidget(PageState state, Element parent) { + String value = getParameterData(state).marshal(); + Element editor = parent.newChildElement(getElementTag(), BEBOP_XML_NS); + + editor.addAttribute("name", getName()); + generateDescriptionXML(state, editor); + + // Set the needed config params so they don't have to be hardcoded in the theme + editor.addAttribute("editor_url", Web.getWebappContextPath().concat(getEditorURL())); + editor.addAttribute("editor_src", Web.getWebappContextPath().concat(getEditorSrc())); + + if (value != null) { + editor.setText(value); + } + + exportAttributes(editor); + + Element config = editor.newChildElement("bebop:config", BEBOP_XML_NS); + config.addAttribute("name", m_config.getName()); + if (m_config.getPath() != null) { + config.addAttribute("path", Web.getWebappContextPath().concat(m_config.getPath())); + } + if (m_hiddenButtons.size() > 0) { + + StringBuffer hiddenButtons = new StringBuffer(); + // list must start and end with a space + hiddenButtons.append(" "); + Iterator hidden = m_hiddenButtons.iterator(); + while (hidden.hasNext()) { + hiddenButtons.append(hidden.next()); + hiddenButtons.append(" "); + } + config.addAttribute("hidden-buttons", hiddenButtons.toString()); + } + Iterator plugins = m_plugins.iterator(); + while (plugins.hasNext()) { + String name = (String) plugins.next(); + Element plugin = editor.newChildElement("bebop:plugin", BEBOP_XML_NS); + plugin.addAttribute("name", name); + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/Date.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/Date.java new file mode 100755 index 000000000..f12f74f75 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/Date.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + +import java.text.DateFormatSymbols; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +import com.arsdigita.util.Assert; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.bebop.parameters.DateParameter; +import com.arsdigita.bebop.parameters.ParameterData; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.Form; +import com.arsdigita.bebop.FormData; +import com.arsdigita.bebop.parameters.*; +// This interface contains the XML element name of this class +// in a constant which is used when generating XML +import com.arsdigita.bebop.util.BebopConstants; + +import com.arsdigita.globalization.GlobalizationHelper; +import com.arsdigita.bebop.util.GlobalizationUtil; + +import com.arsdigita.xml.Element; +import java.text.SimpleDateFormat; +import java.util.Locale; + +/** + * A class representing a date field in an HTML form. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Michael Pih + * @author Sören Bernstein + * @version $Id$ + */ +public class Date extends Widget implements BebopConstants { + + protected OptionGroup m_year; + protected OptionGroup m_month; + protected TextField m_day; + private int m_year_begin; + private int m_year_end; + private Locale m_locale; + private boolean yearAsc = true; + + /** + * Inner class for the year fragment + */ + protected class YearFragment extends SingleSelect { + + protected Date parent; + private boolean autoCurrentYear; //Decide wether to set the current year if year is null + + /** + * Constructor. + * + * @param name + * @param parent + */ + public YearFragment(String name, Date parent) { + super(name); + this.parent = parent; + setHint(GlobalizationUtil.globalize("bebop.date.year.hint")); + } + + /** + * + * @param ps + * + * @return + */ + @Override + protected ParameterData getParameterData(PageState ps) { + Object value = getValue(ps); + if (value == null) { + return null; + } + return new ParameterData(getParameterModel(), value); + } + + /** + * + * @param autoCurrentYear + */ + public void setAutoCurrentYear(final boolean autoCurrentYear) { + this.autoCurrentYear = autoCurrentYear; + } + + /** + * + * @param ps + * + * @return + */ + @Override + public Object getValue(PageState ps) { + ParameterModel model = parent.getParameterModel(); + if (model instanceof IncompleteDateParameter) { + if (((IncompleteDateParameter) model).isYearSkipped()) { + return null; + } + } + Object value = parent.getFragmentValue(ps, Calendar.YEAR); + if ((value == null) && autoCurrentYear) { + Calendar currentTime = GregorianCalendar.getInstance(); + int currentYear = currentTime.get(Calendar.YEAR); + value = new Integer(currentYear); + } + return value; + } + + } + + /** + * + */ + protected class MonthFragment extends SingleSelect { + + protected Date parent; + + public MonthFragment(String name, Date parent) { + super(name); + this.parent = parent; + } + + @Override + protected ParameterData getParameterData(PageState ps) { + Object value = getValue(ps); + if (value == null) { + return null; + } + return new ParameterData(getParameterModel(), value); + } + + @Override + public Object getValue(PageState ps) { + ParameterModel model = parent.getParameterModel(); + if (model instanceof IncompleteDateParameter) { + if (((IncompleteDateParameter) model).isMonthSkipped()) { + return null; + } + } + return parent.getFragmentValue(ps, Calendar.MONTH); + } + + } + + /** + * + */ + protected class DayFragment extends TextField { + + protected Date parent; + + public DayFragment(String name, Date parent) { + super(name); + this.parent = parent; + } + + @Override + protected ParameterData getParameterData(PageState ps) { + Object value = getValue(ps); + if (value == null) { + return null; + } + return new ParameterData(getParameterModel(), value); + } + + @Override + public Object getValue(PageState ps) { + ParameterModel model = parent.getParameterModel(); + if (model instanceof IncompleteDateParameter) { + if (((IncompleteDateParameter) model).isDaySkipped()) { + return null; + } + } + return parent.getFragmentValue(ps, Calendar.DATE); + } + + } + + /** + * Construct a new Date. The model must be a DateParameter + */ + public Date(ParameterModel model) { + super(model); + + if (!(model instanceof DateParameter || model instanceof DateTimeParameter)) { + throw new IllegalArgumentException( + "The Date widget " + model.getName() + + " must be backed by a DateParameter parmeter model"); + } + + String name = model.getName(); + String nameYear = name + ".year"; + String nameMonth = name + ".month"; + String nameDay = name + ".day"; + + Calendar currentTime = GregorianCalendar.getInstance(); + + m_year = new YearFragment(nameYear, this); + m_month = new MonthFragment(nameMonth, this); + m_day = new DayFragment(nameDay, this); + + m_day.setMaxLength(2); + m_day.setSize(2); + + populateMonthOptions(); + + int currentYear = currentTime.get(Calendar.YEAR); + setYearRange(currentYear - 1, currentYear + 3); + + } + + /** + * Constructor. + * + * @param name + */ + public Date(String name) { + this(new DateParameter(name)); + } + + public void setAutoCurrentYear(final boolean autoCurrentYear) { + ((YearFragment) m_year).setAutoCurrentYear(autoCurrentYear); + } + + public void setYearRange(int yearBegin, int yearEnd) { + Assert.isUnlocked(this); + if (yearBegin != m_year_begin || yearEnd != m_year_end) { + m_year_begin = yearBegin; + m_year_end = yearEnd; + + m_year.clearOptions(); + if (this.getParameterModel() instanceof IncompleteDateParameter) { + // Create an empty year entry to unset a date, if either + // a) skipYearAllowed is true + // b) skipDayAllowed is true and skipMonthAllowed is true, to unset a date + if (((IncompleteDateParameter) this.getParameterModel()).isSkipYearAllowed() + || (((IncompleteDateParameter) this.getParameterModel()).isSkipDayAllowed() + && ((IncompleteDateParameter) this.getParameterModel()) + .isSkipMonthAllowed())) { + m_year.addOption(new Option("", "")); + } + } + if (yearAsc) { + for (int year = m_year_begin; year <= m_year_end; year++) { + m_year.addOption(new Option(String.valueOf(year))); + } + } else { + for(int year = m_year_end; year >= m_year_begin; year--) { + m_year.addOption(new Option(String.valueOf(year))); + } + } + } + } + + public boolean getYearAsc() { + return yearAsc; + } + + public void setYearAsc(final boolean yearAsc) { + this.yearAsc = yearAsc; + } + + public void addYear(java.util.Date date) { + Calendar cal = new GregorianCalendar(); + cal.setTime(date); + int year = (cal.get(Calendar.YEAR)); + if (year < m_year_begin) { + m_year.prependOption(new Option(String.valueOf(year))); + } + + if (year > m_year_end) { + m_year.addOption(new Option(String.valueOf(year))); + } + } + + /** + * Returns a string naming the type of this widget. + * + * @return + */ + @Override + public String getType() { + return "date"; + } + + /** + * Sets the MAXLENGTH attribute for the INPUT tag used to render this form + * element. + * + * @param length + */ + public void setMaxLength(int length) { + setAttribute("MAXLENGTH", String.valueOf(length)); + } + + @Override + public boolean isCompound() { + return true; + } + + /** + * The XML tag for this derived class of Widget. + * + * @return + */ + @Override + protected String getElementTag() { + return BEBOP_DATE; + } + + /** + * + * @param ps + * @param parent + */ + @Override + public void generateWidget(PageState ps, Element parent) { + + if (!isVisible(ps)) { + return; + } + + Element date = parent.newChildElement(getElementTag(), BEBOP_XML_NS); + date.addAttribute("name", getParameterModel().getName()); + if (getLabel() != null) { + date.addAttribute("label", (String) getLabel().localize(ps.getRequest())); + } + exportAttributes(date); + generateDescriptionXML(ps, date); + generateLocalizedWidget(ps, date); + + // If Element could be null insert an extra widget to clear entry + if (!hasValidationListener(new NotNullValidationListener())) { + date.newChildElement("NoDate"); + } + } + + // Resepct the localized + public void generateLocalizedWidget(PageState ps, Element date) { + + Locale defaultLocale = Locale.getDefault(); + Locale locale = GlobalizationHelper.getNegotiatedLocale(); + + // Get the current Pattern + // XXX This is really, really, really, really, really, really bad + // but there is no way to get a SimpleDateFormat object for a + // different locale the the system default (the one you get with + // Locale.getDefault();). Also there is now way getting the pattern + // in another way (up until JDK 1.1 there was), so I have to temporarly + // switch the default locale to my desired locale, get a SimpleDateFormat + // and switch back. + Locale.setDefault(locale); + String format = new SimpleDateFormat().toPattern(); + Locale.setDefault(defaultLocale); + + // Repopulate the options for the month select box to get them localized + populateMonthOptions(); + + char[] chars = format.toCharArray(); + for (int i = 0; i < chars.length; i++) { + + // Test for doublettes + if (i >= 1 && chars[i - 1] == chars[i]) { + continue; + } + + switch (chars[i]) { + case 'd': + m_day.generateXML(ps, date); + break; + case 'M': + m_month.generateXML(ps, date); + break; + case 'y': + m_year.generateXML(ps, date); + break; + default: + break; + } + + } + + } + + @Override + public void setDisabled() { + m_month.setDisabled(); + m_day.setDisabled(); + m_year.setDisabled(); + } + + @Override + public void setReadOnly() { + m_month.setReadOnly(); + m_day.setReadOnly(); + m_year.setReadOnly(); + } + + /** + * Sets the Form Object for this Widget. This method will throw an exception if the _form + * pointer is already set. To explicity change the _form pointer the developer must first call + * setForm(null) + * + * @param f the Form Object for this Widget. + * + * @exception IllegalStateException if form already set. + */ + @Override + public void setForm(Form f) { + super.setForm(f); + m_year.setForm(f); + m_month.setForm(f); + m_day.setForm(f); + } + + public Object getFragmentValue(PageState ps, int field) { + Assert.exists(ps, "PageState"); + FormData f = getForm().getFormData(ps); + if (f != null) { + java.util.Date value = (java.util.Date) f.get(getName()); + if (value != null) { + Calendar c = Calendar.getInstance(); + c.setTime(value); + return new Integer(c.get(field)); + } + } + return null; + } + + @Override + public void setClassAttr(String at) { + m_month.setClassAttr(at); + m_year.setClassAttr(at); + m_day.setClassAttr(at); + super.setClassAttr(at); + } + + private void populateMonthOptions() { + + Locale locale = GlobalizationHelper.getNegotiatedLocale(); + + if (m_locale == null || (locale != null && !m_locale.equals(locale))) { + + DateFormatSymbols dfs = new DateFormatSymbols(locale); + String[] months = dfs.getMonths(); + + m_month.clearOptions(); + + if (this.getParameterModel() instanceof IncompleteDateParameter) { + if (((IncompleteDateParameter) this.getParameterModel()).isSkipMonthAllowed()) { + m_month.addOption(new Option("", "")); + } + } + for (int i = 0; i < months.length; i += 1) { + // This check is necessary because + // java.text.DateFormatSymbols.getMonths() returns an array + // of 13 Strings: 12 month names and an empty string. + if (months[i].length() > 0) { + m_month.addOption(new Option(String.valueOf(i), months[i])); + } + } + m_locale = GlobalizationHelper.getNegotiatedLocale(); + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/DateTime.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/DateTime.java new file mode 100755 index 000000000..dc780534f --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/DateTime.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + +import com.arsdigita.bebop.Form; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.parameters.DateTimeParameter; +import com.arsdigita.bebop.parameters.NotNullValidationListener; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.bebop.util.BebopConstants; +import com.arsdigita.xml.Element; + +/** + * A class representing a date and time field in an HTML form. + * (based on the code in Date.java) + * + * @author Sören Bernstein + * @version $Id$ + */ +public class DateTime extends Widget implements BebopConstants { + + private Date m_date; + private Time m_time; + + /** + * Construct a new DateTime. The model must be a DateTimeParameter + * @param model + */ + public DateTime(ParameterModel model) { + this(model, false); + } + + /** + * Construct a new DateTime. The model must be a DateTimeParameter + * @param model + * @param showSeconds + */ + public DateTime(ParameterModel model, boolean showSeconds) { + super(model); + + if (!(model instanceof DateTimeParameter)) { + throw new IllegalArgumentException( + "The DateTime widget " + model.getName() + + " must be backed by a DateTimeParameter parmeter model"); + } + + m_date = new Date(model); + m_time = new Time(model, showSeconds); + } + + public DateTime(String name) { + this(new DateTimeParameter(name)); + } + + public void setYearRange(int startYear, int endYear) { + m_date.setYearRange(startYear, endYear); + } + + /** + * Returns a string naming the type of this widget. + * @return + */ + @Override + public String getType() { + return "dateTime"; + } + + /** + * Sets the MAXLENGTH attribute for the INPUT tag + * used to render this form element. + */ + public void setMaxLength(int length) { + setAttribute("MAXLENGTH", String.valueOf(length)); + } + + public boolean isCompound() { + return true; + } + + /** The XML tag for this derived class of Widget. + */ + @Override + protected String getElementTag() { + return BEBOP_DATETIME; + } + + @Override + public void generateWidget(PageState ps, Element parent) { + + if (!isVisible(ps)) { + return; + } + + Element datetime = parent.newChildElement(getElementTag(), BEBOP_XML_NS); + datetime.addAttribute("name", getParameterModel().getName()); + m_date.generateLocalizedWidget(ps, datetime); + m_time.generateLocalizedWidget(ps, datetime); + + generateDescriptionXML(ps, datetime); + + // If Element could be null insert a extra widget to clear entry + if (!hasValidationListener(new NotNullValidationListener())) { + datetime.newChildElement("NoDateTime"); + } + } + + @Override + public void setDisabled() { + m_date.setDisabled(); + m_time.setDisabled(); + } + + @Override + public void setReadOnly() { + m_date.setReadOnly(); + m_time.setReadOnly(); + } + + /** + * Sets the Form Object for this Widget. This method will throw an + * exception if the _form pointer is already set. To explicity + * change the _form pointer the developer must first call + * setForm(null) + * + * @param the Form Object for this Widget. + * @exception IllegalStateException if form already set. + */ + @Override + public void setForm(Form f) { + super.setForm(f); + m_date.setForm(f); + m_time.setForm(f); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/Deditor.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/Deditor.java new file mode 100755 index 000000000..23342b5c5 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/Deditor.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + + +import com.arsdigita.xml.Element; + +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.form.Widget; +import com.arsdigita.bebop.parameters.ParameterModel; +// This interface contains the XML element name of this class +// in a constant which is used when generating XML +import com.arsdigita.bebop.util.BebopConstants; + + + +/** + * A class representing a textarea field in an HTML form. + * + * @deprecated See {@link DHTMLEditor} + * @author Jim Parsons + */ +public class Deditor extends Widget implements BebopConstants { + + + /** + * Constant for specifying OFF value for the + * WRAP attribute of this image input. See here + * for a description of what this attribute does. */ + + public static final int OFF = 0; + + /** + * Constant for specifying HARD value for the + * WRAP attribute of this image input. * See here + * for a description of what this attribute does. + */ + public static final int HARD = 1; + + /** + * Constant for specifying SOFT value for the + * WRAP attribute of this image input. See here + * for a description of what this attribute does. + */ + public static final int SOFT = 2; + + public Deditor(String name) { + super(name); + } + + public Deditor(ParameterModel model) { + super(model); + } + + + /** + * Returns a string naming the type of this widget. + */ + public String getType() { + return "deditor"; + } + + + /** + * Set the default value (text) + * @deprecated [since 17Aug2001] use {@link Widget#setDefaultValue(Object)} + */ + public void setValue( String text ) { + this.setDefaultValue(text); + } + + /** + * Sets the ROWS attribute for the TEXTAREA tag. + */ + public void setRows(int rows) { + setAttribute("rows", String.valueOf(rows)); + } + + /** + * Sets the COLS attribute for the TEXTAREA tag. + */ + public void setCols(int cols) { + setAttribute("cols", String.valueOf(cols)); + } + + /** + * Sets the COLS attribute for the TEXTAREA tag. + */ + public void setWrap(int wrap) { + String wrapString = null; + + switch (wrap) { + case OFF: + wrapString = "off"; + break; + case HARD: + wrapString = "hard"; + break; + case SOFT: + wrapString = "soft"; + break; + } + + if (wrapString != null) { + setAttribute("wrap", wrapString); + } + } + + /** + * Is this a compound widget? + * @return false + */ + public boolean isCompound() { + return false; + } + + /** The XML tag. + * @return The tag to be used for the top level DOM element + * generated for this type of Widget. */ + protected String getElementTag() { + return "bebop:deditor"; + } + + /** + * Generates the DOM for the textarea widget + *

Generates DOM fragment: + *

<bebop:textarea name=... value=... [onXXX=...]/> + * + */ + public void generateWidget( PageState state, Element parent ) { + + Element deditor = parent.newChildElement(getElementTag(), BEBOP_XML_NS); + + deditor.addAttribute("name", getName()); + + String userAgent = + state.getRequest().getHeader("user-agent").toLowerCase(); + boolean isIE55 = + (userAgent != null && + ((userAgent.indexOf("msie 5.5") != -1) || + (userAgent.indexOf("msie 6") != -1))); + + deditor.addAttribute("isIE55", (new Boolean(isIE55)).toString()); + + + String value = getParameterData(state).marshal(); + if ( value == null ) { + value = ""; + } + Element texter = deditor.newChildElement("bebop:textcontent",BEBOP_XML_NS); + texter.setCDATASection(value); + exportAttributes(deditor); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/Fieldset.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/Fieldset.java new file mode 100644 index 000000000..40922d78a --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/Fieldset.java @@ -0,0 +1,36 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.arsdigita.bebop.form; + +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.SimpleContainer; +import com.arsdigita.globalization.GlobalizedMessage; +import com.arsdigita.xml.Element; + +/** + * A fieldset for form. + * + * @author Sören Bernstein + */ +public class Fieldset extends SimpleContainer { + + GlobalizedMessage m_title; + + public Fieldset(GlobalizedMessage title) { + super("bebop:fieldset", BEBOP_XML_NS); + m_title = title; + } + + @Override + public void generateXML(PageState state, Element p) { + if (isVisible(state)) { + Element parent = generateParent(p); + parent.addAttribute("legend", (String) m_title.localize()); + generateChildrenXML(state, parent); + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/FileUpload.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/FileUpload.java new file mode 100755 index 000000000..41b6da0ec --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/FileUpload.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + +import com.arsdigita.bebop.event.ParameterEvent; +import com.arsdigita.bebop.parameters.GlobalizedParameterListener; +import com.arsdigita.bebop.parameters.ParameterData; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.dispatcher.MultipartHttpServletRequest; +import com.arsdigita.globalization.GlobalizedMessage; + +import javax.servlet.http.HttpServletRequest; + + +/** + * A class representing a file upload widget. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Stas Freidin + * @author Rory Solomon + * @author Michael Pih + * @version $Id$ */ + +public class FileUpload extends Widget { + + public FileUpload(String name) { + this(name, true); + } + + public FileUpload(String name, boolean validateInputFile) { + super(name); + addValidationListener(new FileExistsValidationListener()); + } + + public FileUpload(ParameterModel model) { + this(model, true); + } + + public FileUpload(ParameterModel model, boolean validateInputFile) { + super(model); + addValidationListener(new FileExistsValidationListener()); + } + + /** + * Returns a string naming the type of this widget. + * @return + */ + @Override + public String getType() { + return "file"; + } + + /** + * + * @return + */ + @Override + public boolean isCompound() { + return false; + } + + + /** + * + */ + private class FileExistsValidationListener extends GlobalizedParameterListener { + + public FileExistsValidationListener() { + setError(new GlobalizedMessage("file_empty_or_not_found", getBundleBaseName())); + } + + @Override + public void validate (ParameterEvent e) { + ParameterData data = e.getParameterData(); + HttpServletRequest request = e.getPageState().getRequest(); + String filename = (String) data.getValue(); + + if (!(request instanceof MultipartHttpServletRequest) || + filename == null || + filename.length() == 0) { + return; + } + + if (((MultipartHttpServletRequest) request).getFile(data.getModel() + .getName()) + .length()==0) { + data.addError(filename + " " + getError().localize()); + } + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/FormErrorDisplay.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/FormErrorDisplay.java new file mode 100755 index 000000000..22ec69276 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/FormErrorDisplay.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + +import com.arsdigita.bebop.Form; +import com.arsdigita.bebop.PageErrorDisplay; +import com.arsdigita.bebop.list.ListModelBuilder; +import com.arsdigita.bebop.list.ListModel; +import com.arsdigita.bebop.List; +import com.arsdigita.bebop.FormData; +import com.arsdigita.bebop.PageState; +import com.arsdigita.util.LockableImpl; + +import java.util.Collections; + +/** + * Displays validation errors on the form which were added by the form's + * validation listener. Does not handle errors in the individual parameters; + * these errors are handled by the form's template. This class is not + * a form widget, since it does not produce a value. + * + * @author Stanislav Freidin + * @version $Id$ + * + */ +public class FormErrorDisplay extends PageErrorDisplay { + + private Form m_form; + + /** + * Construct a new FormErrorDisplay + * + * @param form The parent form whose errors will be displayed by + * this widget + */ + public FormErrorDisplay(Form form) { + super(new FormErrorModelBuilder(form)); + m_form = form; + } + + /** + * Return the form whose errors are to be displayed + * @return the form whose errors are to be displayed + */ + public final Form getForm() { + return m_form; + } + + /** + * Determine if there are errors to display + * + * @param state the current page state + * @return true if there are any errors to display; false otherwise + */ + protected boolean hasErrors(PageState state) { + FormData data = m_form.getFormData(state); + return (data != null && data.getErrors().hasNext()); + } + + // A private class which builds a ListModel based on form errors + private static class FormErrorModelBuilder extends LockableImpl + implements ListModelBuilder { + + private Form m_form; + + public FormErrorModelBuilder(Form form) { + super(); + m_form = form; + } + + public ListModel makeModel(List l, PageState state) { + FormData data = m_form.getFormData(state); + if(data == null) { + return new StringIteratorModel(Collections.EMPTY_LIST.iterator()); + } else { + return new StringIteratorModel(data.getErrors()); + } + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/Hidden.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/Hidden.java new file mode 100755 index 000000000..968df902b --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/Hidden.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + +import com.arsdigita.bebop.parameters.ParameterModel; + +/** + * A class representing a hidden HTML form element. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Stas Freidin + * @author Rory Solomon + * @author Michael Pih + * @version $Id$ + */ +public class Hidden extends Widget { + + public Hidden(String name) { + super(name); + } + + public Hidden(ParameterModel model) { + super(model); + } + + /** + * Returns a string naming the type of this widget. + */ + public String getType() { + return "hidden"; + } + + public boolean isCompound() { + return false; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/ImageSubmit.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/ImageSubmit.java new file mode 100755 index 000000000..94728e23e --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/ImageSubmit.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.bebop.util.PanelConstraints; + +/** + * A class representing an image HTML form element. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Rory Solomon + * @author Michael Pih + * @version $Id$ + */ +public class ImageSubmit extends Widget implements PanelConstraints { + + + /** + * Constructor. + * + * @param name + */ + public ImageSubmit(String name) { + super(name); + } + + public ImageSubmit(ParameterModel model) { + super(model); + } + + /** + * Returns a string naming the type of this widget. + * + * @return + */ + @Override + public String getType() { + return "image"; + } + + /** + * Sets the SRC attribute for the INPUT tag + * used to render this form element. + * + * @param location + */ + public void setSrc(String location) { + setAttribute("src",location); + } + + /* + * Sets the ALRT attribute for the INPUT tag + * used to render this form element. + */ + public void setAlt(String alt) { + setAttribute("alt",alt); + } + + /** + * Sets the ALIGN attribute for the INPUT tag + * used to render this form element. Uses the positional constants defined + * in Interface PanelConstraints. + * Note: These may be refactored in future versions. + * + * @param align Symbolic constant denoting the alignment. + */ + public void setAlign(int align) { + String alignString = null; + + switch (align) { + case LEFT: + alignString = "left"; + break; + case RIGHT: + alignString = "right"; + break; + case TOP: + alignString = "top"; + break; + case ABSMIDDLE: + alignString = "absmiddle"; + break; + case ABSBOTTOM: + alignString = "absbottom"; + break; + case TEXTTOP: + alignString = "texttop"; + break; + case MIDDLE: + alignString = "middle"; + break; + case BASELINE: + alignString = "baseline"; + break; + case BOTTOM: + alignString = "botton"; + break; + } + + if (alignString != null) + setAttribute("align",alignString); + } + + @Override + public boolean isCompound() { + return false; + } + + /** + * Callback method for rendering this Image widget in a visitor. + */ + /* public void accept(FormVisitor visitor) throws IOException { + visitor.visitImage(this); + } + */ + + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelect.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelect.java new file mode 100755 index 000000000..cbcdcc65c --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelect.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + +import com.arsdigita.bebop.parameters.ArrayParameter; +// This interface contains the XML element name of this class +// in a constant which is used when generating XML +import com.arsdigita.bebop.util.BebopConstants; + +/** + * A class + * representing an HTML SELECT element. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Rory Solomon + * @author Michael Pih + * @version $Id$ */ +public class MultipleSelect extends Select implements BebopConstants { + + public MultipleSelect(String name) { + super(new ArrayParameter(name)); + } + + /** State that this is a multiple select + * @return true + */ + public boolean isMultiple() + { return true; } + + /** The XML tag for this derived class of Widget. */ + + protected String getElementTag() { + return BEBOP_MULTISELECT; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelectPairWidget.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelectPairWidget.java new file mode 100755 index 000000000..e9d8c14f0 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelectPairWidget.java @@ -0,0 +1,561 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + +import com.arsdigita.bebop.FormStep; +import com.arsdigita.bebop.GridPanel; +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.RequestLocal; +import com.arsdigita.bebop.event.FormInitListener; +import com.arsdigita.bebop.event.FormProcessListener; +import com.arsdigita.bebop.event.FormSectionEvent; +import com.arsdigita.bebop.parameters.ArrayParameter; +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Element; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Iterator; +import java.util.HashMap; +import java.util.Map; +import org.apache.log4j.Logger; + +/** + *

Multiple select widget pair for knowledge types. This FormStep + * displays two multiple select widgets, one which contains possible + * the user may want to add, and the right displays the options that + * are currently applicable.

+ * + *

To use the widget, you should call {@link + * #setLeftMultipleSelect(RequestLocal)} and {@link + * #setRightMultipleSelect(RequestLocal)} and pass in the appropriate + * collections to initialize the MutlipleSelect options. Then, in the + * process listener of the form in which the MultipleSelectPairWidget + * is embedded, call {@link #getSelectedOptions(PageState)} and {@link + * #getUnselectedOptions(PageState)} to get the chosen values. The process + * listener for the parent form must use the Submit.isSelected(ps) so + * that the process listener can distinguish between different types + * of form submits.

+ * + *

Note that the right multiple select can be empty and does not need + * to be set. This class also uses a relatively inefficient + * implementation of removeOption in {@link OptionGroup OptionGroup} + * so that operations run in O(N^2). This can be reduced to O(N) with + * a more optimal implementation of OptionGroup.

+ * + * @see Option + * @see OptionGroup + * @version $Id$ + */ +public class MultipleSelectPairWidget extends FormStep { + + private static final Logger s_log = + Logger.getLogger(MultipleSelectPairWidget.class); + + private Hidden m_addSelectOptions; + private Hidden m_removeSelectOptions; + private MultipleSelect m_addSelect; + private MultipleSelect m_removeSelect; + private Submit m_addSubmit; + private Submit m_removeSubmit; + private RequestLocal m_addSelectDataSource; + private RequestLocal m_removeSelectDataSource; + private RequestLocal m_selectsPopulated; + private RequestLocal m_leftSelectMap = null; + private RequestLocal m_rightSelectMap = null; + private boolean m_leftSideChanges; + + private String m_qualifier; + + private final static int RIGHT = 1; + private final static int LEFT = 2; + + // Empty array for internal use. Should be part of a generic utility class. + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + // Configuration options. + private int m_multipleSelectSize = 20; + + /** + * This create a standard MultipleSelectPairWidget with the + * default names used for internal widgets. + */ + public MultipleSelectPairWidget() { + this(null); + } + + public MultipleSelectPairWidget(String nameQualifier) { + super(nameQualifier + "MultipleSelectPairWidget", new GridPanel(3)); + m_qualifier = nameQualifier; + + m_addSelectOptions = + new Hidden(new ArrayParameter(qualify("addSelectOptions"))); + m_removeSelectOptions = new Hidden + (new ArrayParameter(qualify("removeSelectOptions"))); + + m_addSelect = new MultipleSelect(qualify("leftSelect")); + m_addSelect.setSize(m_multipleSelectSize); + m_removeSelect = new MultipleSelect(qualify("rightSelect")); + m_removeSelect.setSize(m_multipleSelectSize); + setLeftSideChanges(true); + + m_addSubmit = new Submit(qualify("->"), " -> "); + m_removeSubmit = new Submit(qualify("<-"), " <- "); + + GridPanel centerPanel = new GridPanel(1); + centerPanel.add(m_addSubmit); + centerPanel.add(m_removeSubmit); + + add(m_addSelect, GridPanel.LEFT); + add(centerPanel, GridPanel.CENTER); + add(m_removeSelect, GridPanel.RIGHT); + add(m_addSelectOptions); + add(m_removeSelectOptions); + + m_selectsPopulated = new RequestLocal(); + addInitListener(new MultipleSelectPairFormInitListener + (m_selectsPopulated)); + addProcessListener(new MultipleSelectPairFormProcessListener + (m_selectsPopulated)); + } + + public boolean isSelected(PageState ps) { + return m_addSubmit.isSelected(ps) || m_removeSubmit.isSelected(ps); + } + + /** + * @param collection A collection of Option objects + */ + public void setLeftMultipleSelect(RequestLocal collection) { + m_addSelectDataSource = collection; + } + + /** + * This lets the user pass in a RequestLocal that returns + * a java.util.Map that contains the option value as the + * key and the actual option as the map value. + * When populating the left select, the system will use + * this map before falling back to the default map. + */ + public void setLeftMultipleSelectMap(RequestLocal map) { + m_leftSelectMap = map; + } + + /** + * This lets the user pass in a RequestLocal that returns + * a java.util.Map that contains the option value as the + * key and the actual option as the map value. + * When populating the left select, the system will use + * this map before falling back to the default map. + */ + public void setRightMultipleSelectMap(RequestLocal map) { + m_rightSelectMap = map; + } + + /** + * This returns the left select widget so that callers + * can have access to the underlying parameters and + * other features (e.g. in case they need to add a + * ParameterListener) + */ + public Widget getLeftSelect() { + return m_addSelect; + } + + /** + * This returns the left select widget so that callers + * can have access to the underlying parameters and + * other features (e.g. in case they need to add a + * ParameterListener) + */ + public Widget getRightSelect() { + return m_removeSelect; + } + + + /** + * @param doesChange This indicates whether the items in the + * "to add" select box are removed as they are added to + * the "to remove" select box. That is, as choices are + * selected, should they be removed from the list of + * choices? This defaults to true. + */ + public void setLeftSideChanges(boolean doesChange) { + m_leftSideChanges = doesChange; + } + + /** + * This returns an indication of whether or not the left + * multiple select changes as items are moved from the left + * to the right. + */ + public boolean leftSideChanges() { + return m_leftSideChanges; + } + + + /** + * @param collection A collection of Option objects + */ + public void setRightMultipleSelect(RequestLocal collection) { + m_removeSelectDataSource = collection; + } + + /** + * Returns the selected options, those selected from the left hand widget + * + * @return array of options + * @post return != null + */ + public String[] getSelectedOptions(PageState ps) { + String[] options = (String[]) m_removeSelectOptions.getValue(ps); + // Probably unneccessary, as widget should be populated with EMPTY_STRING_ARRAY in init listener + // if there is no data. + if (null == options) { + options = EMPTY_STRING_ARRAY; + } + return options; + } + + /** + * Returns the unselected options, those removed from the right hand widget + * + * @return array of options + * @post return != null + */ + public String[] getUnselectedOptions(PageState ps) { + String[] options = (String[]) m_addSelectOptions.getValue(ps); + // Probably unneccessary, as widget should be populated with EMPTY_STRING_ARRAY in init listener + // if there is no data. + if (null == options) { + options = EMPTY_STRING_ARRAY; + } + return options; + } + + public void generateXML(PageState state, Element element) { + // if the page has not been populated then it need to + // be populated from the hidden variables. Otherwise, + // nothing will be displayed in the multi-select boxes. + if (!Boolean.TRUE.equals(m_selectsPopulated.get(state)) && + isInitialized(state)) { + List addOptions = new ArrayList(); + List removeOptions = new ArrayList(); + + String[] unselected = getUnselectedOptions(state); + for (int i = 0; i < unselected.length; i++) { + String option = unselected[i]; + addOptions.add(option); + } + + String[] selected = getSelectedOptions(state); + for (int i = 0; i < selected.length; i++) { + String option = selected[i]; + removeOptions.add(option); + } + + + m_selectsPopulated.set(state, Boolean.TRUE); + generateOptionValues(state, addOptions, removeOptions, + setupOptionMap(state)); + m_addSelect.addOption(getEmptyOption(), state); + m_removeSelect.addOption(getEmptyOption(), state); + } + super.generateXML(state, element); + } + + /** + * This changes the name of the parameter so that it is possible + * to include several of these on the same page. + */ + private String qualify(String property) { + if (m_qualifier != null) { + return m_qualifier + "_" + property; + } else { + return property; + } + } + + + /** + * @size The number of rows to display in the multiple selects. + */ + public void setMultipleSelectSize(int size) { + m_multipleSelectSize = size; + m_removeSelect.setSize(m_multipleSelectSize); + m_addSelect.setSize(m_multipleSelectSize); + } + + private HashMap setupOptionMap(PageState ps) { + // We put all of our options into a HashMap so that we can add the + // Option object to the destination MultipleSelect. + HashMap optionsMap = new HashMap(); + Collection addOptions = (Collection) m_addSelectDataSource.get(ps); + + Iterator i; + Option option; + + i = addOptions.iterator(); + while ( i.hasNext() ) { + option = (Option) i.next(); + optionsMap.put(option.getValue(), option); + } + + if ( m_removeSelectDataSource != null ) { + Collection removeOptions = (Collection) m_removeSelectDataSource.get(ps); + if ( removeOptions != null ) { + i = removeOptions.iterator(); + while ( i.hasNext() ) { + option = (Option) i.next(); + if (optionsMap.get(option.getValue()) == null) { + optionsMap.put(option.getValue(), option); + } + } + } + } + + return optionsMap; + } + + private void generateOptionValues(PageState ps, List addOptions, + List removeOptions, + HashMap m_optionsMap) { + Iterator iter; + + iter = addOptions.iterator(); + while ( iter.hasNext() ) { + String s = (String) iter.next(); + Option o = getOption(ps, m_optionsMap, s, LEFT); + // it is possible to be null if for some reason the key, s, is + // not found any of the maps + if (o != null) { + m_addSelect.addOption(o, ps); + } + } + + iter = removeOptions.iterator(); + while ( iter.hasNext() ) { + String s = (String) iter.next(); + Option o = getOption(ps, m_optionsMap, s, RIGHT); + // it is possible to be null if for some reason the key, s, is + // not found any of the maps + if (o != null) { + m_removeSelect.addOption(o, ps); + } + } + } + + + /** + * This looks at the request locals set in setRightMultipleSelectMap + * and setLeftMultipleSelectMap before falling back on the default + * mapping that was auto-generated. If the value is found + * in the passed in map then that value is used. Otherwise, the + * value is located in the default mapping + */ + private Option getOption(PageState state, Map optionMapping, String key, + int side) { + if (side == RIGHT) { + if (m_rightSelectMap != null) { + Map map = (Map)m_rightSelectMap.get(state); + if (map.get(key) != null) { + return (Option)map.get(key); + } + } + } else { + if (m_leftSelectMap != null) { + Map map = (Map)m_leftSelectMap.get(state); + if (map.get(key) != null) { + return (Option)map.get(key); + } + } + } + return (Option)optionMapping.get(key); + } + + private class MultipleSelectPairFormProcessListener + implements FormProcessListener { + + // This is to allow a call back to set an item as being + // initialized + private RequestLocal m_processed; + MultipleSelectPairFormProcessListener(RequestLocal processed) { + m_processed = processed; + } + + public void process(FormSectionEvent evt) { + PageState ps = evt.getPageState(); + + if (!m_addSubmit.isSelected(ps) + && !m_removeSubmit.isSelected(ps)) { + return; + } + + m_processed.set(ps, Boolean.TRUE); + + HashMap m_optionsMap; + List addOptions = new ArrayList(); + List removeOptions = new ArrayList(); + + m_optionsMap = setupOptionMap(ps); + + // We first update the array lists that contain the list + // of unselected options based on the contents of the + // hidden form variables. + updateUnselectedOptions(ps, addOptions, removeOptions); + + // Then we update those array lists based on what the user + // moves. + if ( m_addSubmit.isSelected(ps) ) { + String[] selectedArray = (String[]) m_addSelect.getValue(ps); + if ( selectedArray != null ) { + List selectedAddOptions = Arrays.asList(selectedArray); + Iterator iter = selectedAddOptions.iterator(); + while ( iter.hasNext() ) { + String s = (String) iter.next(); + // we only want to add the item if it has not + // already been added + if (!removeOptions.contains(s)) { + removeOptions.add(s); + } + if (leftSideChanges()) { + addOptions.remove(s); + } + } + } + } + + if ( m_removeSubmit.isSelected(ps) ) { + String[] selectedArray = (String[]) m_removeSelect.getValue(ps); + if ( selectedArray != null ) { + List selectedRemoveOptions = Arrays.asList(selectedArray); + Iterator iter = selectedRemoveOptions.iterator(); + while ( iter.hasNext() ) { + String s = (String) iter.next(); + removeOptions.remove(s); + // if the left side does not change then the + // item was never removed from the addOptions so + // it does not need to be added back. + if (leftSideChanges()) { + addOptions.add(s); + } + } + } + } + + // Next, we put the full list of options back into the hidden. + // we have to convert this to a String[]...otherwise we + // can get a ClassCastException when used within a Wizard + String[] newValues = (String[]) addOptions.toArray(EMPTY_STRING_ARRAY); + m_addSelectOptions.setValue(ps, newValues); + + // We do the same conversion for the new values + newValues = (String[]) removeOptions.toArray(EMPTY_STRING_ARRAY); + m_removeSelectOptions.setValue(ps, newValues); + + // We finally generate the option values. + generateOptionValues(ps, addOptions, removeOptions, m_optionsMap); + m_addSelect.addOption(getEmptyOption(), ps); + m_removeSelect.addOption(getEmptyOption(), ps); + } + + private void updateUnselectedOptions(PageState ps, List addOptions, + List removeOptions) { + // We add the unselected options back to the MultipleSelects. + String[] unselected = getUnselectedOptions(ps); + for (int i = 0; i < unselected.length; i++) { + String s = unselected[i]; + addOptions.add(s); + } + + String[] selected = getSelectedOptions(ps); + for (int i = 0; i < selected.length; i++) { + String s = selected[i]; + removeOptions.add(s); + } + + + } + } + + private class MultipleSelectPairFormInitListener implements FormInitListener { + private RequestLocal m_initialized; + MultipleSelectPairFormInitListener(RequestLocal initialized ) { + m_initialized = initialized; + } + + public void init(FormSectionEvent evt) { + PageState ps = evt.getPageState(); + m_initialized.set(ps, Boolean.TRUE); + + String[] addOptionsForHidden = EMPTY_STRING_ARRAY; + String[] removeOptionsForHidden = EMPTY_STRING_ARRAY; + + Assert.exists(m_addSelectDataSource, + "You must provide some options for the " + + "user to choose!"); + + Collection addOptions = (Collection) m_addSelectDataSource.get(ps); + if (addOptions.size() > 0) { + Iterator iter = addOptions.iterator(); + addOptionsForHidden = new String[addOptions.size()]; + int idx = 0; + while ( iter.hasNext() ) { + Option option = (Option) iter.next(); + m_addSelect.addOption(option, ps); + addOptionsForHidden[idx++] = option.getValue(); + } + + } + + if ( m_removeSelectDataSource != null ) { + Collection c = (Collection) m_removeSelectDataSource.get(ps); + if ( c != null && c.size() > 0 ) { + removeOptionsForHidden = new String[c.size()]; + Iterator iter = c.iterator(); + int idx = 0; + while ( iter.hasNext() ) { + Option option = (Option) iter.next(); + m_removeSelect.addOption(option, ps); + removeOptionsForHidden[idx++] = option.getValue(); + } + } + } + + m_addSelectOptions.setValue(ps, addOptionsForHidden); + m_removeSelectOptions.setValue(ps, removeOptionsForHidden); + m_addSelect.addOption(getEmptyOption(), ps); + m_removeSelect.addOption(getEmptyOption(), ps); + } + } + + private Option getEmptyOption() { + return new Option("", + new Label("        " + + "        " + + "        " + + "        " + + "        " + + "        ", + false)); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/Option.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/Option.java new file mode 100755 index 000000000..65960c7cc --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/Option.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.DescriptiveComponent; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.parameters.ParameterData; +import com.arsdigita.util.Assert; +import com.arsdigita.bebop.util.BebopConstants; +import com.arsdigita.xml.Element; + +/** + * A class representing an option of a widget. + * + * The Option consist of two parts: + * - a value, used by the background task to process the option + * - a display component, used to display the option to the user in the GUI, + * usually a Label (title), but may be e.g. an image as well. + + * @author Rory Solomon + * @author Michael Pih + * + * $Id$ + */ +public class Option extends DescriptiveComponent { + + /** The value of the option, used by the background task to process the + * option. + * NOTE: The display component, the label, is provided by parent class! */ + private String m_value; + /** The display component for the user in the GUI. It's usually a Label, + * but may be e.g. an image as well. */ + private Component m_component; + private OptionGroup m_group; + private boolean m_isSelectOption; + + // /////////////////////////////////////////////////////////////////////// + // Constructor Section + // + + /** + * A (too) simple Constructor which uses a String as value as well as + * display component. + * + * @param value A String used as value as well as display component. + * @deprecated use Option(value,component) instead + */ + public Option(String value) { + this(value, value); + } + + /** + * Constructor creates an Option whose label part consisting of a string. + * This results in a badly globalized label part. The localization depends + * on the language selected at the time the Option is created. + * + * @param value + * @param label + * @deprecated use Option(value,component) instead + */ + public Option(String value, String label) { + setValue(value); + setLabel(label); + } + + /** + * Constructor creates an Option whose label part consisting of a Component, + * usually a Label(GlobalizedMessage). + * This constructor should be used to create a fully globalized and + * localized user interface. + * + * @param value + * @param label + */ + public Option(String value, Component label) { + setValue(value); + setComponent(label); + } + + // /////////////////////////////////////////////////////////////////////// + // Getter/Setter Section + // + + /** + * Retrieves the value part of an option. + * @return the value part of this option. + */ + public final String getValue() { + return m_value; + } + + /** + * Sets of modifies the value of on option. + * @param value new value part of the option + */ + public final void setValue(String value) { + m_value = value; + } + + + /** + * Retrieves the display part of the option. + * @return the display component for this option + */ + public final Component getComponent() { + return m_component; + } + + /** + * Sets of modifies the display component of an option. + * + * @param component the display component for this option + */ + public final void setComponent(Component component) { + Assert.isUnlocked(this); + m_component = component; + } + + /** + * Sets of modifies the display component of an option providing a Label. + * The label is internally stored as a component. + * + * @param label + */ + public final void setLabel(Label label) { + setComponent(label); + } + + /** + * This sets the display component using a String. It results in a badly + * globalized UI + * + * @param label String to use as the display component + * @deprecated Use {@link #setComponent(Component component)} instead + */ + public final void setLabel(String label) { + setComponent(new Label(label)); + } + + + /** + * + * @param group + */ + public final void setGroup(OptionGroup group) { + Assert.isUnlocked(this); + Assert.exists(group); + m_group = group; + m_isSelectOption = BebopConstants.BEBOP_OPTION.equals(m_group.getOptionXMLElement()); + } + + /** + * + * @return + */ + public final OptionGroup getGroup() { + return m_group; + } + + /** + * Retrieves the name (identifier) of the option group containing this + * option. Don't know the purpose of this. + * + * @return The name (identifier) of the option group this option belongs + * to + */ + public String getName() { + return m_group.getName(); + } + + + /** + * Sets the ONFOCUS attribute for the HTML tags that compose + * this element. + * @param javascriptCode + */ + public void setOnFocus(String javascriptCode) { + setAttribute(Widget.ON_FOCUS,javascriptCode); + } + + /** + * Sets the ONBLUR attribute for the HTML tags that compose + * this element. + * @param javascriptCode + */ + public void setOnBlur(String javascriptCode) { + setAttribute(Widget.ON_BLUR,javascriptCode); + } + + /** + * Sets the ONSELECT attribute for the HTML tags that compose + * this element. + * @param javascriptCode + */ + public void setOnSelect(String javascriptCode) { + setAttribute(Widget.ON_SELECT,javascriptCode); + } + + /** + * Sets the ONCHANGE attribute for the HTML tags that compose + * this element. + * @param javascriptCode + */ + public void setOnChange(String javascriptCode) { + setAttribute(Widget.ON_CHANGE,javascriptCode); + } + + + /** + * Sets the ON_KEY_UP attribute for the HTML tags that compose + * this element. + * @param javascriptCode + **/ + + public void setOnKeyUp(String javascriptCode) { + setAttribute(Widget.ON_KEY_UP, javascriptCode); + } + + /** + * Sets the ONCLICK attribute for the HTML tags that compose + * this element. + * @param javascriptCode + */ + public void setOnClick(String javascriptCode) { + setAttribute(Widget.ON_CLICK,javascriptCode); + } + + private ParameterData getParameterData(PageState s) { + return m_group.getParameterData(s); + } + + public boolean isSelected(ParameterData data) { + if (data == null || data.getValue() == null) { + return false; + } + Object value = data.getValue(); + + Object[] selectedValues; + if (value instanceof Object[]) { + selectedValues = (Object[])value; + } else { + selectedValues = new Object[] {value}; + } + String optionValue = getValue(); + + if (optionValue == null || selectedValues == null) { + return false; + } + for (Object selectedValue : selectedValues) { + if (selectedValue != null + && optionValue.equalsIgnoreCase(selectedValue.toString())) { + return true; + } + } + return false; + } + + /** + * Generate XML depending on what OptionGr. + * + * @param s + * @param e + */ + @Override + public void generateXML(PageState s, Element e) { + Element option = e.newChildElement(m_group.getOptionXMLElement(), BEBOP_XML_NS); + if ( ! m_isSelectOption ) { + option.addAttribute("name", getName()); + } + option.addAttribute("value", getValue()); + + if (m_component != null) { + m_component.generateXML(s, option); + } else { + (new Label()).generateXML(s, option); + } + + exportAttributes(option); + if ( isSelected(getParameterData(s)) ) { + if ( m_isSelectOption ) { + option.addAttribute("selected", "selected"); + } else { + option.addAttribute("checked", "checked"); + } + } + } + + /** + * Kludge to live with the fact that options don't do their own + * printing. Don't use this method, it will go away ! + * + * @deprecated Will be removed without replacement once option handling + * has been refactored. + */ + final void generateAttributes(Element target) { + exportAttributes(target); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/OptionGroup.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/OptionGroup.java new file mode 100755 index 000000000..e271f29d5 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/OptionGroup.java @@ -0,0 +1,574 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.form; + +import com.arsdigita.bebop.Form; +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.RequestLocal; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.parameters.ParameterData; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.bebop.parameters.ParameterModelWrapper; +import com.arsdigita.bebop.util.BebopConstants; +import com.arsdigita.globalization.GlobalizationHelper; +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Element; +import java.text.Collator; + +import java.util.Iterator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +import org.apache.log4j.Logger; + +/** + * A class representing any widget that contains a list of options. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Rory Solomon + * @author Michael Pih + * @version $Id$ + */ +public abstract class OptionGroup extends Widget implements BebopConstants { + + private static final Logger LOGGER = Logger.getLogger(OptionGroup.class); + + /** + * The XML element to be used by individual options belonging to this group. + * This variable has to be initialized by every subclass of OptionGroup. + * LEGACY: An abstract method would be the better design, but changing it + * would break the API. + */ + //protected String m_xmlElement; + + // this only needs to be an ArrayList for multiple selection option groups + private List m_selected; + private List
+ * line 4 column 22 - Warning: replacing unexpected
by + * line 5 column 1 - Warning: discarding unexpected + * line 6 column 2 - Error: is not recognized! + * line 6 column 2 - Warning: discarding unexpected + * line 6 column 9 - Warning: discarding unexpected + * This document has errors that must be fixed before + * using HTML Tidy to generate a tidied up version. + * + * If there are no errors (meaning, there are only warning or + * no warnings), then the last chunk does not appear. + */ + + String summary = null; + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if ( token.startsWith(LINE_WARNING_START) ) { + if ( !canBeIgnored(token) ) { + warningsAndErrors.add(StringUtils.quoteHtml(token)); + m_hasWarnings = true; + } + } else { + summary = token; + if ( st.hasMoreTokens() ) { + summary += st.nextToken(""); + } + break; + } + } + + StringBuffer sb = new StringBuffer(); + + if ( warningsAndErrors.size() > 0 ) { + if ( warningsAndErrors.size() == 1 ) { + sb.append("

").append((String) warningsAndErrors.get(0)); + sb.append("

").append(LINE_SEPARATOR); + } else { + sb.append("
    ").append(LINE_SEPARATOR); + for (Iterator i=warningsAndErrors.iterator(); i.hasNext(); ) { + sb.append("
  1. ").append((String) i.next()); + sb.append("
  2. ").append(LINE_SEPARATOR); + } + sb.append("
").append(LINE_SEPARATOR); + } + } + if ( summary != null ) { + sb.append("

").append(StringUtils.quoteHtml(summary)).append("

"); + m_hasErrors = !isWarning(summary); + } + m_formattedMessage = sb.toString(); + } + } + + private static class LockableProperties extends Properties { + private boolean m_isLocked = false; + + public LockableProperties() { + super(); + } + + public LockableProperties(Properties defaults) { + super(defaults); + } + + public void lock() { + m_isLocked = true; + } + + private void checkIfLocked() { + if (m_isLocked) { + throw new RuntimeException + ("The object cannot be modified once initialized."); + } + } + + public Object setProperty(String key, String value) { + checkIfLocked(); + return super.setProperty(key, value); + } + + public void load(InputStream is) throws IOException { + checkIfLocked(); + super.load(is); + } + + public Object put(Object key, Object value) { + checkIfLocked(); + return super.put(key, value); + } + + public void putAll(java.util.Map t) { + checkIfLocked(); + super.putAll(t); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/parameters/TimeParameter.java b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/TimeParameter.java new file mode 100755 index 000000000..0a40c169d --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/TimeParameter.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.parameters; + +import com.arsdigita.globalization.Globalization; +import com.arsdigita.globalization.GlobalizationHelper; + +import com.arsdigita.util.StringUtils; + +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import javax.servlet.http.HttpServletRequest; + + + +/** + * A class that represents the model for a time form parameter. + * + * @see com.arsdigita.bebop.parameters.DateTimeParameter + * @author Dave Turner + * @version $Id$ + */ +public class TimeParameter extends ParameterModel +{ + + public TimeParameter ( String name ) { + super(name); + } + + /** + * This method returns a new Calendar object that is manipulated + * within transformValue to create a Date Object. This method should + * be overridden if you wish to use a Calendar other than the + * lenient GregorianCalendar. + * + * @param request the servlet request from which Locale can be + * extracted if needed + * + * @return a new Calendar object + * */ + protected Calendar getCalendar(HttpServletRequest request) { + return new GregorianCalendar(); + } + + + /** + * Computes a dateTime object from multiple parameters in the + * request. This method searches for parameters named + * getName() + ".hour", + * getName() + ".minute", + * getName() + ".second", and + * getName() + ".amOrPm". + * */ + public Object transformValue(HttpServletRequest request) + throws IllegalArgumentException { + + Calendar c = getCalendar(request); + c.clear(); + + String hour = Globalization.decodeParameter(request, getName()+".hour"); + String minute = Globalization.decodeParameter(request, getName()+".minute"); + String second = Globalization.decodeParameter(request, getName()+".second"); + String amOrPm = Globalization.decodeParameter(request, getName()+".amOrPm"); + + if (StringUtils.emptyString(hour) && + StringUtils.emptyString(minute) && + StringUtils.emptyString(second)) { + return transformSingleValue(request); + } + + if (!StringUtils.emptyString(hour)) { + int hourInt = Integer.parseInt(hour); +/* Das ist alles Blödsinn. Beim 24-Stundenformat brauchen wir das sowieso nicht. +Beim 12-Stunden-Formato müßte es, wenn überhaupt, anderherum sein: Aus einer +eingetragenen 0 in den Stunden muß eine 12 werden. ABER: Die Informationen +werden in einem Calendar-Object gespeichert, das intern immer 24-Stunden-Format +verwendet. Das 12-Stunden-Format ist eine Frage der Formatierung und somit +hier irrelevant. Es bleibt zu testet, ob ein 12:00 AM im Caendar-Object tatsächlich +zu 0:00 Uhr wird. + if ((hourInt == 12) && has12HourClock()) { + hourInt = 0; + } +*/ + c.set(Calendar.HOUR, hourInt); + } + + if (!StringUtils.emptyString(minute)) { + c.set(Calendar.MINUTE, Integer.parseInt(minute)); + } + + if (!StringUtils.emptyString(second)) { + c.set(Calendar.SECOND, Integer.parseInt(second)); + } + + if ( amOrPm != null ) { + c.set(Calendar.AM_PM, Integer.parseInt(amOrPm)); + } + + return c.getTime(); + } + + + public Object unmarshal ( String encoded ) { + try { + return new Date(Long.parseLong(encoded)); + } catch ( NumberFormatException ex ) { + throw new IllegalArgumentException("Cannot unmarshal time '" + + encoded + "': " + ex.getMessage()); + } + } + + public String marshal ( Object value ) { + return Long.toString(((Date)value).getTime()); + } + + public Class getValueClass () { + return Date.class; + } + + private boolean has12HourClock() { + Locale locale = GlobalizationHelper.getNegotiatedLocale(); + DateFormat format_12Hour = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.US); + DateFormat format_locale = DateFormat.getTimeInstance(DateFormat.SHORT, locale); + + String midnight = ""; + try { + midnight = format_locale.format(format_12Hour.parse("12:00 AM")); + } catch (ParseException ignore) { + } + + return midnight.contains("12"); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/parameters/TrimmedStringParameter.java b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/TrimmedStringParameter.java new file mode 100755 index 000000000..ee56545a0 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/TrimmedStringParameter.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.parameters; + +import javax.servlet.http.HttpServletRequest; + +/** + * A class that represents the model for String form parameters. + * This class extends StringParameter and differs by calling trim() + * on non-null values. This class is useful for fields you want to + * put in the DB without leading and trailng spaces + * + * @author Karl Goldstein + * @author Uday Mathur + * @version $Id$ + */ +public class TrimmedStringParameter extends StringParameter { + + public TrimmedStringParameter(String name) { + super(name); + } + + public Object transformValue(HttpServletRequest request) + throws IllegalArgumentException { + + String requestValue = (String)super.transformValue(request); + if (requestValue!=null) { + requestValue = requestValue.trim(); + } + return (requestValue==null) ? null : unmarshal(requestValue); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/parameters/TypeCheckValidationListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/TypeCheckValidationListener.java new file mode 100755 index 000000000..3f07f7e47 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/TypeCheckValidationListener.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.parameters; + +import com.arsdigita.bebop.parameters.ParameterData; +import com.arsdigita.bebop.event.ParameterEvent; +import com.arsdigita.globalization.GlobalizedMessage; + +/** + * Verifies that the + * parameter's type is the expected type + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Stas Freidin + * @author Rory Solomon + * @version $Id$ + */ +public class TypeCheckValidationListener extends GlobalizedParameterListener { + + private Class m_type; + + public TypeCheckValidationListener(Class type) { + this.m_type = type; + } + + public TypeCheckValidationListener(Class type, GlobalizedMessage error) { + this.m_type = type; + setError(error); + } + + public void validate (ParameterEvent e) { + + ParameterData data = e.getParameterData(); + Object obj = data.getValue(); + + if (obj == null && data.isTransformed()) { + return; + } + + if (getError() == null) { + setError(new GlobalizedMessage( + "type_check", + getBundleBaseName(), + new Object[] { + data.getName(), + m_type.getName(), + obj.toString(), + obj.getClass().getName() + } + )); + } + + if (!m_type.isInstance(obj)) { + data.addError(getError()); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URIParameter.java b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URIParameter.java new file mode 100644 index 000000000..142bd4398 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URIParameter.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2009 Permeance Technologies Pty Ltd. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.parameters; + +/** + * A parameter that contains a URI formatted according to RFC2396: + * + * @see URIValidationListener + * + * @author terry_permeance + */ +public class URIParameter extends StringParameter { + + public URIParameter(String name) { + super(name); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URIValidationListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URIValidationListener.java new file mode 100644 index 000000000..df8e2f2d4 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URIValidationListener.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) Permeance Technologies Pty Ltd. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.parameters; + +import java.net.URI; +import java.net.URISyntaxException; + +import com.arsdigita.bebop.FormProcessException; +import com.arsdigita.bebop.event.ParameterEvent; +import com.arsdigita.bebop.event.ParameterListener; +import com.arsdigita.globalization.GlobalizedMessage; + +/** + * A parameter listener that ensures a parameter is a URI formatted + * according to RFC2396: + * + *
+ *
+ * The following examples illustrate URI that are in common use.
+ * 
+ * ftp://ftp.is.co.za/rfc/rfc1808.txt
+ *    -- ftp scheme for File Transfer Protocol services
+ * 
+ * gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles
+ *    -- gopher scheme for Gopher and Gopher+ Protocol services
+ * 
+ * http://www.math.uio.no/faq/compression-faq/part1.html
+ *    -- http scheme for Hypertext Transfer Protocol services
+ * 
+ * mailto:mduerst@ifi.unizh.ch
+ *    -- mailto scheme for electronic mail addresses
+ * 
+ * news:comp.infosystems.www.servers.unix
+ *    -- news scheme for USENET news groups and articles
+ * 
+ * telnet://melvyl.ucop.edu/
+ *    -- telnet scheme for interactive services via the TELNET Protocol
+ * 
+ *
+ * + * @author terry_permeance + */ +public class URIValidationListener extends GlobalizedParameterListener { + + public URIValidationListener() { + setError(new GlobalizedMessage("uri_parameter_is_invalid", getBundleBaseName())); + } + + /** + * @see ParameterListener#validate(ParameterEvent) + */ + public void validate(ParameterEvent e) throws FormProcessException { + + ParameterData d = e.getParameterData(); + String value = (String)d.getValue(); + + if (value != null && value.length() > 0) { + try { + URI uri = new URI(value); + if (!uri.isAbsolute()) { + d.addError(this.getError()); + } + } catch (URISyntaxException ex) { + d.addError(this.getError()); + } + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLParameter.java b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLParameter.java new file mode 100755 index 000000000..74f74b601 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLParameter.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.parameters; + +import com.arsdigita.globalization.Globalization; +import java.net.MalformedURLException; +import java.net.URL; +import javax.servlet.http.HttpServletRequest; + +/** + * A class that represents the model for URL form parameters. + * + * @author Karl Goldstein + * @author Uday Mathur + * @author Rory Solomon + * @version $Id$ + */ +public class URLParameter extends StringParameter { + + public URLParameter(String name) { + super(name); + } + + @Override + public Object transformValue(HttpServletRequest request) + throws IllegalArgumentException { + + String requestValue = Globalization.decodeParameter(request, getName()); + if (requestValue==null) { + return null; + } + URL URLValue; + try { + URLValue = new URL(requestValue); + } catch (MalformedURLException e) { + try { + URLValue = new URL("HTTP://" + requestValue); + } catch (MalformedURLException e2) { + throw new IllegalArgumentException + (getName() + " is not a valid URL: '" + requestValue + + "'; " + e2.getMessage()); + } + } + return unmarshal(requestValue); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLTokenValidationListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLTokenValidationListener.java new file mode 100755 index 000000000..3c2c9df4e --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLTokenValidationListener.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.parameters; + +import com.arsdigita.bebop.event.ParameterEvent; +import com.arsdigita.globalization.GlobalizedMessage; +//import com.arsdigita.bebop.parameters.ParameterData; + +import org.apache.oro.text.perl.Perl5Util; + +/** + * Verifies that the parameter's value is composed only of character which are + * valid as part on an URL name (the token). + * That is only alpha-numeric, underscore, or hyphen characters. + * [a-zA-Z_0-9\-] + * + * Note: An empty string will pass the validation tests. + * + * @author Michael Pih + * @version $Id$ + */ +public class URLTokenValidationListener extends GlobalizedParameterListener { + + /** match 1 or more instances of a non-alpha-numeric character */ + private static final String NON_KEYWORD_PATTERN = "/[^a-zA-Z_0-9\\-]+/"; + + /** + * Default Constructor setting a predefined label as error message. + */ + public URLTokenValidationListener() { + setError(new GlobalizedMessage("bebop.parameters.must_be_valid_part_of_url", + getBundleBaseName() ) + ); + } + + /** + * Constructor taking a label specified as key into a resource bundle to + * customize the error message. + * + * @param label key into the resource bundle + * @deprecated use URLTokenValidationListener(GlobalizedMessage error) + */ + public URLTokenValidationListener(String label) { + setError(new GlobalizedMessage(label, getBundleBaseName())); + } + + /** + * Constructor taking a GlobalizedMessage as error message to display. + * + * @param error GloblizedMessage taken as customized error message. + */ + public URLTokenValidationListener(GlobalizedMessage error) { + setError(error); + } + + + /** + * Validates the parameter by checking if the value is a valid keyword. + * A keyword is defined as any combination of alph-numeric characters, + * hyphens, and/or underscores. [a-zA-Z_0-9\-] + * + * Note: An empty string will pass the validation tests. + * + * @param event The parameter event + */ + @Override + public void validate(ParameterEvent event) { + ParameterData data = event.getParameterData(); + Object value = data.getValue(); + + Perl5Util util = new Perl5Util(); + if ( !util.match(NON_KEYWORD_PATTERN, value.toString()) ) { + return; + } + data.addError(getError()); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLValidationListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLValidationListener.java new file mode 100755 index 000000000..5cfcbf2b5 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/URLValidationListener.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.parameters; + +import com.arsdigita.bebop.event.ParameterEvent; +import com.arsdigita.bebop.event.ParameterListener; +import com.arsdigita.bebop.FormProcessException; +//import com.arsdigita.bebop.parameters.ParameterData; +import java.net.MalformedURLException; +import java.net.URL; + +/** + * + * + */ +public class URLValidationListener implements ParameterListener { + + @Override + public void validate(ParameterEvent e) + throws FormProcessException { + + ParameterData d = e.getParameterData(); + String value = (String)d.getValue(); + + if (!value.equals("")) { + try { + new URL(value); + } catch (MalformedURLException ex) { + d.invalidate(); + d.addError("Please enter a URL"); + } + } +} +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/parameters/UniqueStringValidationListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/UniqueStringValidationListener.java new file mode 100644 index 000000000..21a4bf3fc --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/UniqueStringValidationListener.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) Permeance Technologies Pty Ltd. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.parameters; + +import com.arsdigita.bebop.FormProcessException; +import com.arsdigita.bebop.event.ParameterEvent; +import com.arsdigita.globalization.GlobalizedMessage; +import com.arsdigita.util.Assert; + +/** + * Validates that a {@link String} property of a {@link DomainObject} is unique. + * + * @author + * terry_permeance + */ +public class UniqueStringValidationListener extends GlobalizedParameterListener { + + public UniqueStringValidationListener(String baseDataObjectType, + String propertyKey, + ParameterModel domainParameter) { + + this.setError(new GlobalizedMessage("parameter_not_unique", this + .getBundleBaseName())); + Assert.exists(baseDataObjectType); + Assert.exists(propertyKey); + Assert.exists(domainParameter); + m_baseDataObjectType = baseDataObjectType; + m_propertyKey = propertyKey; + m_domainParameter = domainParameter; + } + + public void validate(ParameterEvent e) throws FormProcessException { + +// ParameterData data = e.getParameterData(); +// String propertyValue = (data.getValue() == null ? null : String.valueOf( +// data.getValue())); +// +// if (propertyValue != null && propertyValue.length() > 0) { +// // Get the current domain object +// DomainObject domainObject = (DomainObject) e.getPageState() +// .getValue(m_domainParameter); +// +// // Check if there are any existing matches +// DataCollection collection = SessionManager.getSession().retrieve( +// m_baseDataObjectType); +// collection.addEqualsFilter(m_propertyKey, propertyValue); +// while (collection.next()) { +// if (domainObject == null || !collection.getDataObject().getOID() +// .equals(domainObject.getOID())) { +// data.addError(this.getError()); +// break; +// } +// } +// } + } + + private final String m_baseDataObjectType; + + private final String m_propertyKey; + + private final ParameterModel m_domainParameter; + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/parameters/WordValidationListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/WordValidationListener.java new file mode 100755 index 000000000..904923827 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/parameters/WordValidationListener.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.parameters; + +import com.arsdigita.bebop.event.ParameterEvent; +import com.arsdigita.bebop.event.ParameterListener; +import com.arsdigita.bebop.FormProcessException; +import com.arsdigita.bebop.parameters.ParameterData; + +import org.apache.oro.text.perl.Perl5Util; + +import org.apache.log4j.Logger; + +public class WordValidationListener implements ParameterListener { + private static final Logger s_log = + Logger.getLogger( WordValidationListener.class ); + + public void validate(ParameterEvent e) + throws FormProcessException { + + ParameterData d = e.getParameterData(); + String value = (String)d.getValue(); + + if( null == value ) return; + + if( s_log.isDebugEnabled() ) { + s_log.debug( "Name: " + d.getName() + ", Value: " + value ); + } + + Perl5Util re = new Perl5Util(); + if (!re.match("/^\\s*\\w*\\s*$/", value)) { + d.invalidate(); + d.addError("Please enter a single word"); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/util/Attributes.java b/ccm-core/src/main/java/com/arsdigita/bebop/util/Attributes.java new file mode 100755 index 000000000..192a0caa2 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/util/Attributes.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.util; + +import com.arsdigita.util.Assert; +import com.arsdigita.util.Lockable; + +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Collection; + +/** + * This class represents a set of key-value pairs, for use in + * extending the XML attributes of Bebop components. + * + * @version $Id: Attributes.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class Attributes implements Lockable, Cloneable { + + /** + * Map of attributes. + */ + private HashMap m_attributes; + + private boolean m_locked; + + /** + * Creates an Attributes object. + */ + public Attributes() { + m_attributes = new HashMap(); + m_locked = false; + } + + /** + * Clone the attributes. The clone is not locked and has its own set of + * attributes and values. + * @post ! ((Attributes) return).isLocked() + */ + public Object clone() throws CloneNotSupportedException { + Attributes result = (Attributes) super.clone(); + result.m_attributes = (HashMap) m_attributes.clone(); + result.m_locked = false; + return result; + } + + /** + *

Sets an arbitrary attribute for inclusion in the HTML tags that + * compose element. For standard attributes in the HTML 4.0 + * specification, use of this method has the same effect as the + * specific mutator method provided for each attribute.

+ * + *

Setting an attribute name to null + * removes it.

+ * + * @param name The name of the attribute + * @param value The value to assign the named attribute + */ + public void setAttribute(String name, String value) { + Assert.isUnlocked(this); + name = name.toLowerCase(); + m_attributes.put(name, value); + } + + /** + * Return the value of an attribute. + * + * @pre name != null + * + * @param name the name of the attribute + * @return the value set previously with + * {@link #setAttribute setAttribute} + */ + public String getAttribute(String name) { + return (String) m_attributes.get(name.toLowerCase()); + } + + /** + * Return a collection of all of the attribute keys represented. + * This, along with {@link #getAttribute(String name)} allows + * you to iterate through all of the attributes. All elements + * of the Collection are Strings + */ + public Collection getAttributeKeys() { + return m_attributes.keySet(); + } + + + /** + * Copy all attributes into the given DOM Element. This will + * override any preexisting Element attributes of the same names. + */ + public void exportAttributes(com.arsdigita.xml.Element target) { + Iterator attributesIterator = m_attributes.entrySet().iterator(); + + while (attributesIterator.hasNext()) { + Map.Entry entry = (Map.Entry) attributesIterator.next(); + + if (entry.getValue() != null) { + target.addAttribute((String) entry.getKey(), + (String) entry.getValue()); + } + } + } + + public void lock() { + m_locked = true; + } + + public final boolean isLocked() { + return m_locked; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/util/BebopConstants.java b/ccm-core/src/main/java/com/arsdigita/bebop/util/BebopConstants.java new file mode 100755 index 000000000..081de303a --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/util/BebopConstants.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.util; + +/** + * An interface that contains all the Bebop + * constants that are used by Bebop components in generating the + * output XML. + * + * @author Jim Parsons + * @version $Id: BebopConstants.java 1224 2006-06-18 22:28:30Z apevec $ + */ +public interface BebopConstants { + + String BEBOP_CHECKBOXGROUP = "bebop:checkboxGroup"; + String BEBOP_CHECKBOX = "bebop:checkbox"; + String BEBOP_DATE = "bebop:date"; + String BEBOP_DATETIME = "bebop:datetime"; + String BEBOP_TIME = "bebop:time"; + String BEBOP_MULTISELECT = "bebop:multiSelect"; + String BEBOP_OPTION = "bebop:option"; + String BEBOP_RADIOGROUP = "bebop:radioGroup"; + String BEBOP_RADIO = "bebop:radio"; + String BEBOP_SELECT = "bebop:select"; + String BEBOP_TEXTAREA = "bebop:textarea"; + String BEBOP_DHTMLEDITOR = "bebop:dhtmleditor"; + String BEBOP_FCKEDITOR = "bebop:fckeditor"; + String BEBOP_XINHAEDITOR = "bebop:xinha"; + String BEBOP_FORMWIDGET = "bebop:formWidget"; + String BEBOP_FORMERRORS = "bebop:formErrors"; + String BEBOP_PORTAL = "bebop:portal"; + String BEBOP_PORTLET = "bebop:portlet"; + String BEBOP_BOXPANEL = "bebop:boxPanel"; + String BEBOP_CELL = "bebop:cell"; + String BEBOP_COLUMNPANEL = "bebop:columnPanel"; + String BEBOP_GRIDPANEL = "bebop:gridPanel"; + String BEBOP_BORDER = "bebop:cell"; + String BEBOP_PAD = "bebop:pad"; + String BEBOP_PADFRAME = "bebop:padFrame"; + String BEBOP_PANELROW = "bebop:panelRow"; + String BEBOP_LIST = "bebop:list"; + String BEBOP_TABLE = "bebop:table"; + String BEBOP_TABLEBODY = "bebop:tbody"; + String BEBOP_TABLEROW = "bebop:trow"; + String BEBOP_SEG_PANEL = "bebop:segmentedPanel"; + String BEBOP_SEGMENT = "bebop:segment"; + String BEBOP_SEG_BODY = "bebop:segmentBody"; + String BEBOP_SEG_HEADER = "bebop:segmentHeader"; + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/util/Color.java b/ccm-core/src/main/java/com/arsdigita/bebop/util/Color.java new file mode 100755 index 000000000..10a649323 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/util/Color.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.util; + +/** + *

A class for Bebop color parameters.

+ * + *
+ * private Page buildSomePage() {
+ *     Page page = new Page("Some Page");
+ *
+ *     Label label = new Label("Some Text");
+ *
+ *     // Make the label text green.
+ *     label.setColor(new Color(0,255,0));
+ *
+ *     // Here's another way of doing the same thing.
+ *     label.setColor(Color.green);
+ *
+ *     page.add(label);
+ *
+ *     page.lock();
+ *
+ *     return page;
+ * }
+ * 
+ * + * @author Jim Parsons + * @version $Id: Color.java 287 2005-02-22 00:29:02Z sskracic $ + * @deprecated without replacement. Bebop must never directly specify design + * properties but semantic properties. tjhe theme engine + * decides about the design and the display! + */ +public class Color { + + private int m_red = 255; + private int m_green = 255; + private int m_blue = 255; + + /** An instance of the color black. */ + public static final Color black = new Color(0,0,0); + + /** An instance of the color blue. */ + public static final Color blue = new Color(0,0,255); + + /** An instance of the color cyan. */ + public static final Color cyan = new Color(0,255,255); + + /** An instance of the color darkGray. */ + public static final Color darkGray = new Color(169,169,169); + + /** An instance of the color gray. */ + public static final Color gray = new Color(128,128,128); + + /** An instance of the color green. */ + public static final Color green = new Color(0,128,0); + + /** An instance of the color lightGray. */ + public static final Color lightGray = new Color(211,211,211); + + /** An instance of the color magenta. */ + public static final Color magenta = new Color(255,0,255); + + /** An instance of the color orange. */ + public static final Color orange = new Color(255,165,0); + + /** An instance of the color pink. */ + public static final Color pink = new Color(255,192,203); + + /** An instance of the color red. */ + public static final Color red = new Color(255,0,0); + + /** An instance of the color white. */ + public static final Color white = new Color(255,255,255); + + /** An instance of the color yellow. */ + public static final Color yellow = new Color(255,255,0); + + /** An instance of the color nobukoBlue, an exotic medium torquoise. */ + public static final Color nobukoBlue = new Color(72,209,204); + + /** + * Make a color from the constituents red, green, and blue. Each + * color argument is an integer in the range of 0 to 255. + * + * @param red the amount of red. Must be an int in the range of 0 to 255. + * @param green the amount of green. Must be an int in the range of 0 to + * 255. + * @param blue the amount of blue. Must be an int in the range of 0 to 255. + * @deprecated without replacement. Bebop must never directly specify design + * properties but semantic properties. tjhe theme engine + * decides about the design and the display! + */ + public Color(int redValue, int greenValue, int blueValue) { + if (redValue >= 0 && redValue < 256) { + m_red = redValue; + } + + if (greenValue >= 0 && greenValue < 256) { + m_green = greenValue; + } + + if (blueValue >= 0 && blueValue < 256) { + m_blue = blueValue; + } + } + + /** + * Make a color from the constituents red, green, and blue. Each + * color argument is a float in the range of 0 to 1. + * + * @param red the amount of red. Must be a float in the range of 0 to 1. + * @param green the amount of green. Must be a float in the range of 0 to + * 1. + * @param blue the amount of blue. Must be a float in the range of 0 to 1. + */ + public Color(float redValue, float greenValue, float blueValue) { + if (redValue >= 0.0f && redValue <= 1.0f) { + m_red = (int)(255 * redValue); + } + + if (greenValue >= 0.0f && greenValue <= 1.0f) { + m_green = (int)(255 * greenValue); + } + + if(blueValue >= 0.0f && blueValue <= 1.0f) { + m_blue = (int)(255 * blueValue); + } + } + + /** + * Return a string with hex values padded out two places with a + * leading 0. + * + * @return a string representing this color. + */ + public String toString() { + String result, redString, greenString, blueString; + + if (m_red < 16) { + redString = "0" + Integer.toHexString(m_red); + } else { + redString = Integer.toHexString(m_red); + } + + if (m_green < 16) { + greenString = "0" + Integer.toHexString(m_green); + } else { + greenString = Integer.toHexString(m_green); + } + + if (m_blue < 16) { + blueString = "0" + Integer.toHexString(m_blue); + } else { + blueString = Integer.toHexString(m_blue); + } + + result = redString + greenString + blueString; + + return result; + } + + /** + * Produce an HTML hex-based representation of this color. + * + * @return an HTML hex color. + */ + public String toHTMLString() { + String result = "#" + this.toString(); + + return result; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/util/GlobalizationUtil.java b/ccm-core/src/main/java/com/arsdigita/bebop/util/GlobalizationUtil.java new file mode 100755 index 000000000..e0458a410 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/util/GlobalizationUtil.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.util; + +import com.arsdigita.globalization.Globalized; +import com.arsdigita.globalization.GlobalizedMessage; + +/** + * Compilation of methods to simplify the handling of globalizing keys. + * Basically it adds the name of package's resource bundle files to the + * globalize methods and forwards to GlobalizedMessage, shortening the + * method invocation in the various application classes. + * + * @version $Revision: #6 $ $Date: 2004/08/16 $ + */ +public class GlobalizationUtil implements Globalized { + + /** Name of Java resource files to handle CMS's globalisation. */ + private static final String BUNDLE_NAME = "com.arsdigita.bebop.BebopResources"; + + /** + * Returns a globalized message using the package specific bundle, + * provided by BUNDLE_NAME. + * @param key + * @return + */ + public static GlobalizedMessage globalize(String key) { + return new GlobalizedMessage(key, BUNDLE_NAME); + } + + /** + * Returns a globalized message object, using the package specific bundle, + * as specified by BUNDLE_NAME. Also takes in an Object[] of arguments to + * interpolate into the retrieved message using the MessageFormat class + * (i.e. {0}, {1},... for adding variable strings). + * @param key + * @param args + * @return new instance of a globalized message + */ + public static GlobalizedMessage globalize(String key, Object[] args) { + return new GlobalizedMessage(key, BUNDLE_NAME, args); + + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/util/PanelConstraints.java b/ccm-core/src/main/java/com/arsdigita/bebop/util/PanelConstraints.java new file mode 100644 index 000000000..5657d652c --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/util/PanelConstraints.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 Peter Boy, University of Bremen. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +package com.arsdigita.bebop.util; + +/** + * An interface that contains positional constraints used by the panel classes + * (BoxPanel, ColumnPanel and GridPanel) in generating the output XML. + * + * Used by some other classes as well which have to position box elements (e.g. + * c.ad.bebop.form.ImageSubmit) + * + * @author Peter Boy (pb@zes.uni-bremen.de) + * @version $Id: PanelConstraints.java 1224 2014-06-18 22:28:30Z $ + */ +public interface PanelConstraints { + + /** + * Left-align a component. + */ + public static final int LEFT = 1 ; // << 0; + + /** + * Center a component. + */ + public static final int CENTER = 1 << 1; + + /** + * Right-align a component. + */ + public static final int RIGHT = 1 << 2; + + /** + * Align the top of a component. + */ + public static final int TOP = 1 << 3; + + /** + * Align the middle of a component. + */ + public static final int MIDDLE = 1 << 4; + + /** + * Align the bottom of a component. + */ + public static final int BOTTOM = 1 << 5; + + /** + * Lay out a component across the full width of the panel. + */ + public static final int FULL_WIDTH = 1 << 6; + + /** + * Insert the child component assuming it is printed in a table with the + * same number of columns. + */ + public static final int INSERT = 1 << 7; + + /** + * Constant for specifying ABSMIDDLE alignment of this image input. See the + * + * W3C HTML 4.01 Specification for a description of this attribute. + */ + public static final int ABSMIDDLE = 1 << 8; + + /** + * Constant for specifying ABSBOTTOM alignment of this image input. See the + * + * W3C HTML 4.01 Specification for a description of this attribute. + */ + public static final int ABSBOTTOM = 1 << 9; + + /** + * Constant for specifying ABSBOTTOM alignment of this image input. See the + * + * W3C HTML 4.01 Specification for a description of this attribute. + */ + public static final int TEXTTOP = 1 << 10; + + /** + * Constant for specifying ABSBOTTOM alignment of this image input. See the + * + * W3C HTML 4.01 Specification for a description of this attribute. + */ + public static final int BASELINE = 1 << 11; + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/util/SequentialMap.java b/ccm-core/src/main/java/com/arsdigita/bebop/util/SequentialMap.java new file mode 100755 index 000000000..489b94f7d --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/util/SequentialMap.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.util; + +/** + * A map that keeps its entries in a fixed sequence. All iterators returned + * by this class, for example by entrySet().iterator(), are + * guaranteed to return the entries in the order in which they were put in + * the map. This implementation allows null for both the key + * or the associated value for a map entry. + * + *

+ * Almost all of the map operations, for example {@link #get get} or {@link + * #containsKey containsKey} require time linear in the size of the map, + * making this map only suitable for small map sizes. + * + * @author David Lutterkort + * @version $Id: SequentialMap.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class SequentialMap extends com.arsdigita.util.SequentialMap { + + + /** + * Creates an empty SequentialMap. + */ + public SequentialMap() { + super(); + } + + /** + * Find an entry with the given key. key may be null. + * + *

+ * Requires time linear in the size of the map + * + * @param key the key to find + * @return the index with key key or -1 if no such entry + * exists. + */ + public int findKey(Object key) { + return indexOf(key); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/util/Size.java b/ccm-core/src/main/java/com/arsdigita/bebop/util/Size.java new file mode 100755 index 000000000..176d90c4f --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/util/Size.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.util; + +/** + *

A class for Bebop size parameters.

+ * + *
+ * private Page buildSomePage() {
+ *     Page page = new Page("Some Page");
+ *
+ *     // Put a 10-pixel margin around the contents of this page.
+ *     page.setMargin(new Size(10));
+ *
+ *     // Or, instead, put a 10% margin around it.
+ *     page.setMargin(new Size(10, UNIT_PERCENT));
+ *
+ *     page.lock();
+ *
+ *     return page;
+ * }
+ * 
+ * + * @author Justin Ross + * @author Jim Parsons + * @author Christian + * Brechbühler + * @version $Id: Size.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class Size { + + private float m_scalar; + private String m_unitAbbreviation = ""; + + /** + * Constant for describing sizes in pixels. + */ + public static final int UNIT_PIXEL = 1; + + /** + * Constant for describing a component in terms of percent size + * relative to its container. + */ + public static final int UNIT_PERCENT = 2; + + /** + * Construct a new Size. Classes extending Size should call super + * with the abbreviation of their unit. + * + * @param scalar a simple magnitude. Note that this value may + * be negative. + * @param unitAbbreviation an unit abbreviation for use when the + * size is printed. + * @pre unitAbbreviation != null + */ + protected Size(float scalar, String unitAbbreviation) { + m_scalar = scalar; + m_unitAbbreviation = unitAbbreviation; + } + + /** + * Construct a new Size in pixels. + * + * @param numPixels a simple magnitude. Note that this value may + * be negative. + */ + public Size(int numPixels) { + this((float)(numPixels), ""); + } + + /** + * Construct a new Size using the type indicated in unitEnum. + * unitEnum is any of the UNIT_* constants defined in this class. + * + * @param scalar a simple magnitude. Note that this value may be + * negative. + * @param unitEnum a unit type. + */ + public Size(float scalar, int unitEnum) { + m_scalar = scalar; + + if (unitEnum == UNIT_PIXEL) { + m_unitAbbreviation = ""; + } else if (unitEnum == UNIT_PERCENT) { + m_unitAbbreviation = "%"; + } else { + throw new IllegalArgumentException + ("Bad argument for unitEnum in Size constructor."); + } + } + + /** + * Return the size as a string. This string will be used in + * writing the style attributes of Bebop XML. + * + * @return this Size as a string for inclusion in XML. + * @post return != null */ + public String toString() { + String sizeAsString = Float.toString(m_scalar) + m_unitAbbreviation; + + return sizeAsString; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/util/Traversal.java b/ccm-core/src/main/java/com/arsdigita/bebop/util/Traversal.java new file mode 100755 index 000000000..be55c4130 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/util/Traversal.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.util; + +import java.util.Iterator; +import java.util.Set; +import java.util.HashSet; +import com.arsdigita.bebop.Component; +import org.apache.log4j.Logger; + +/** + *

+ * + *

A utility class for walking down a tree of Bebop components and + * performing some work on each one.

+ * + *

Uses a filter to perform the action only on certain components. + * This filter may be used to skip only individual components or entire + * subtrees. The default filter matches all components.

+ * + * @version $Id: Traversal.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public abstract class Traversal { + private static final Logger s_log = Logger.getLogger(Traversal.class); + + /** + * If test returns PERFORM_ACTION, + * then the action is performed on the component and its children. + * (This is the default.) + */ + public final static int PERFORM_ACTION = 0; + + /** + * If test returns SKIP_COMPONENT, + * then the current component is skipped but its descendants are still + * traversed. + */ + public final static int SKIP_COMPONENT = 1; + + /** + * If test returns SKIP_SUBTREE, + * then the current component and all of its descendants are skipped. + */ + public final static int SKIP_SUBTREE = 2; + + + private Set m_visiting = null; + + { + if (s_log.isDebugEnabled()) { + m_visiting = new HashSet(); + } + } + + /** + * Defines the action to be performed on each node. Users of this + * class should override this method with behavior for their + * particular domain. + * + * @param c the component on which to perform this action. + */ + protected abstract void act(Component c); + + /** + * Invoke {@link #act} on this component, and then do the same for + * each of its children for which the supplied + * test condition is true. + * + * @param c the component on which to call {@link #act}. */ + public void preorder(Component c) { + if (s_log.isDebugEnabled() && m_visiting.contains(c)) { + s_log.debug("Cycle detected at component " + c + + "; visiting nodes: " + m_visiting); + throw new IllegalStateException + ("Component " + c + " is part of a cycle"); + } + + //s_log.debug("preorder called for component " + c.toString()); + + int flag = test(c); + + if (flag == PERFORM_ACTION) { + act(c); + } + + if (flag != SKIP_SUBTREE) { + if (s_log.isDebugEnabled()) { + m_visiting.add(c); + } + + for (Iterator i = c.children(); i.hasNext(); ) { + preorder ((Component) i.next()); + } + } + + if (s_log.isDebugEnabled()) { + m_visiting.remove(c); + } + } + + /** + * The default component test returns PERFORM_ACTION + * to act on all components in the tree. Override this method + * to supply your own component test. + * @param c the component to test + * @return by default returns PERFORM_ACTION on all + * components. + */ + protected int test(Component c) { + return PERFORM_ACTION; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/util/package.html b/ccm-core/src/main/java/com/arsdigita/bebop/util/package.html new file mode 100755 index 000000000..bbd73a730 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/util/package.html @@ -0,0 +1,15 @@ + + + + Bebop Utilities + + + +

+ +Utility classes that are used throughout Bebop. + +

+ + + diff --git a/ccm-core/src/main/java/com/arsdigita/dispatcher/AbortRequestSignal.java b/ccm-core/src/main/java/com/arsdigita/dispatcher/AbortRequestSignal.java new file mode 100644 index 000000000..4c45bce01 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/dispatcher/AbortRequestSignal.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.dispatcher; + +/** + * When thrown, this throwable will percolate up the call stack to + * the top level to abort all processing of this request. The top + * level (BaseDispatcherServlet) will treat it as a normal request + * and try to commit the transaction + */ + +public class AbortRequestSignal extends Error { + + // extending error is a bit of a misnomer but it's what + // we want: an unchecked exception that won't get caught + // by catch (Exception). +} diff --git a/ccm-core/src/main/java/com/arsdigita/dispatcher/DirectoryListingException.java b/ccm-core/src/main/java/com/arsdigita/dispatcher/DirectoryListingException.java new file mode 100644 index 000000000..a30d47629 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/dispatcher/DirectoryListingException.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.dispatcher; + +/** + * Trown when a directory exists without an index file. Intended to signal that + * code that lists the contents of a directory may want to be triggered. + * + * @author Bill Schneider + * @version $Id$ + */ + +public class DirectoryListingException extends Exception { + + public DirectoryListingException() { + super(); + } + + public DirectoryListingException(String s) { + super(s); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherConstants.java b/ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherConstants.java new file mode 100644 index 000000000..f693d9488 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherConstants.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.dispatcher; + +interface DispatcherConstants { + + /** + * Attribute name for the Throwable object saved when a JSP + * handles an error with the errorPage directive. + */ + final static String JSP_EXCEPTION_ATTRIBUTE = + "javax.servlet.jsp.jspException"; + + /** + * Attribute name for the URI that caused an error + * when the servlet container forwards to an error page. + */ + final static String ERROR_REQUEST_ATTRIBUTE = + "javax.servlet.error.request_uri"; + + /** + * Attribute name to indicate that the dispatcher is within + * a request--that is, request start listeners have run but + * request end listeners haven't yet. + */ + final static String REENTRANCE_ATTRIBUTE = + "com.arsdigita.dispatcher.inside_request"; + + /** + * The attribute name for an included resource URI after + * a servlet include (nesting). + */ + final static String INCLUDE_URI = + "javax.servlet.include.request_uri"; + + /** + * The attribute where we store the current RequestContext + * object. + */ + final static String REQUEST_CONTEXT_ATTR = + "com.arsdigita.dispatcher.RequestContext"; + + /** + * The attribute where we store the original HttpServletRequest + * object when we need to wrap the servlet request. + */ + final static String ORIGINAL_REQUEST_ATTR = + "com.arsdigita.dispatcher.OriginalRequest"; + + /** + * The attribute where we store the wrapped servlet request + * object when we need to restore the original request object + * for a forward/include. + */ + final static String WRAPPED_REQUEST_ATTR = + "com.arsdigita.dispatcher.WrappedRequest"; + + /** + * The session attribute where we store an identifier for the + * previous request that made a redirect. This prohibits us from + * following a redirect until after the request that generated the + * redirect commits its transaction. + */ + final static String REDIRECT_SEMAPHORE = + "com.arsdigita.dispatcher.redirect_semaphore"; + + /** + * The application attribute (in ServletContext) where we store the + * list of welcome files from web.xml. + */ + final static String WELCOME_FILES = + "com.arsdigita.dispatcher.welcomefiles"; + + + final static String DISPATCHER_PREFIX_ATTR = + "com.arsdigita.dispatcher.DispatcherPrefix"; +} diff --git a/ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherHelper.java b/ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherHelper.java new file mode 100644 index 000000000..47b660056 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherHelper.java @@ -0,0 +1,1173 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.dispatcher; + +//import com.arsdigita.kernel.Kernel; +import com.arsdigita.kernel.KernelConfig; +import com.arsdigita.util.Assert; +import com.arsdigita.util.ParameterProvider; +import com.arsdigita.util.StringUtils; +import com.arsdigita.util.URLRewriter; +import com.arsdigita.web.ParameterMap; +import com.arsdigita.web.RedirectSignal; +import com.arsdigita.web.URL; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Set; +import java.util.TimeZone; + +import javax.mail.MessagingException; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.jsp.PageContext; + +import org.apache.log4j.Logger; + +/** + * Class static helper methods for request dispatching. + * Contains various generally useful procedural abstractions. + * + * @author Bill Schneider + * @since 4.5 + * @version $Id$ + */ +public final class DispatcherHelper implements DispatcherConstants { + + /** Internal logger instance to faciliate debugging. Enable logging output + * by editing /WEB-INF/conf/log4j.properties int hte runtime environment + * and set com.arsdigita.dispatcher.DispatcherHelper=DEBUG + * by uncommenting or adding the line. */ + private static final Logger s_log = Logger.getLogger(DispatcherHelper.class); + private static String s_webappCtx; + private static String s_staticURL; + private static boolean s_cachingActive; + private static int s_defaultExpiry; + private static DispatcherConfig s_config; + public static SimpleDateFormat rfc1123_formatter; + private static boolean initialized = false; + + static void init() { + if (initialized) { + return; + } + + rfc1123_formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); + rfc1123_formatter.setTimeZone(TimeZone.getTimeZone("GMT")); + + // set the defaults + s_config = getConfig(); + s_staticURL = s_config.getStaticURLPrefix(); + s_defaultExpiry = s_config.getDefaultExpiryTime().intValue(); + s_cachingActive = s_config.isCachingActive(); + + initialized = true; + } + + /** The current HttpServletRequest. */ + private static final ThreadLocal s_request = new ThreadLocal(); + + /** null constructor, private so no one can instantiate! */ + private DispatcherHelper() { + } + + /** + * Return default cache expiry. + * Default is specified in the configuration file (registry) if not + * otherweise set. + * + * @return default cache expiry + */ + public static int getDefaultCacheExpiry() { + init(); + return s_defaultExpiry; + } + + static void setDefaultCacheExpiry(int expiry) { + init(); + s_defaultExpiry = expiry; + } + + public static boolean isCachingActive() { + init(); + return s_cachingActive; + } + + static void setCachingActive(boolean status) { + init(); + s_cachingActive = status; + } + + /** + * Returns the URL path (relative to the webapp root) for the + * current (calling) resource. This works around the quirk that, + * if servlet A includes servlet B, calling getRequestURI() in B + * returns "A" and not "B". + * + * @param req + * @return the URL path (relative to the webapp root) for the currently + * executing resource. + */ + public static String getCurrentResourcePath(HttpServletRequest req) { + String attr = (String) req.getAttribute(INCLUDE_URI); + String str; + if (attr == null) { + str = req.getRequestURI(); + if (str.indexOf("?") > -1) { + str = str.substring(0, str.indexOf("?")); + } + } else { + str = attr; + } + int startIndex = req.getContextPath().length(); + str = str.substring(startIndex); + // fix-up URL -- sometimes broken (Tomcat serving error pages) + if (str.startsWith("//")) { + str = str.substring(1); + } + return str; + } + + /** + * Gets the application context from the request attributes. + * @param req + * @return the application context from the request attributes. + */ + public static RequestContext getRequestContext(HttpServletRequest req) { + return (RequestContext) req.getAttribute(REQUEST_CONTEXT_ATTR); + } + + public static RequestContext getRequestContext() { + return (RequestContext) getRequest().getAttribute(REQUEST_CONTEXT_ATTR); + } + + public static String getDispatcherPrefix(HttpServletRequest req) { + return (String) req.getAttribute(DISPATCHER_PREFIX_ATTR); + } + + public static void setDispatcherPrefix(HttpServletRequest req, + String val) { + req.setAttribute(DISPATCHER_PREFIX_ATTR, val); + } + + /** + * Sets the current request context as a request attribute for + * later retrieval. + * + * @param req the current request object + * @param ac the current request context + * @post DispatcherHelper.getRequestContext(req) == ac + */ + public static void setRequestContext(HttpServletRequest req, RequestContext ac) { + req.setAttribute(REQUEST_CONTEXT_ATTR, ac); + } + + private static void forwardHelper(javax.servlet.RequestDispatcher rd, + HttpServletRequest req, + HttpServletResponse resp) + throws ServletException, IOException { + + Object attr = req.getAttribute(INCLUDE_URI); + + // Yes this does mean we're throwing away any POSTed + // data from a MultipartHttpServletRequest, but we've + // got to do what Servlet API specs say. Specifically + // tomcat assumes it gets back the original request :( + // So we need to go through hoops to 'unrestore' the + // original request when it comes back to us in just + // a second... + // Of course if the request disappears off to a 3rd + // party servlet we're screwed + req = restoreOriginalRequest(req); + s_log.debug("Forwarding the request object " + req); + if (attr != null) { + rd.include(req, resp); + req.setAttribute(INCLUDE_URI, attr); + } else { + try { + rd.forward(req, resp); + } catch (IllegalStateException e) { + rd.include(req, resp); + } + } + } + + /** + * Forwards the request from this resource to another resource at + * the servlet-container level. This is a wrapper for + * javax.servlet.RequestDispatcher + * and is intended to hide the fact that you can't "forward" a request + * once you've done an "include" in it. + * @param path the URL of the resource, relative to the webapp + * root. For example, if you request a JSP page with /context/foo/bar, + * you would call this method with path == /foo/bar. + * @param req the current request + * @param resp the current response + * @param sctx the current servlet context + * @exception java.io.IOException may be propagated from target resource + * @exception javax.servlet.ServletException may be + * propagated from target resource + */ + public static void forwardRequestByPath(String path, + HttpServletRequest req, + HttpServletResponse resp, + ServletContext sctx) + throws IOException, ServletException { + RequestDispatcher rd = sctx.getRequestDispatcher(path); + forwardHelper(rd, req, resp); + } + + /** + * Equivalent to forwardRequestByPath(path, req, resp, + * DispatcherHelper.getRequestContext(req).getServletContext()). + * @param path + * @param req + * @param resp + * @throws java.io.IOException + * @throws javax.servlet.ServletException + */ + public static void forwardRequestByPath(String path, + HttpServletRequest req, + HttpServletResponse resp) + throws IOException, ServletException { + ServletContext sctx = + DispatcherHelper.getRequestContext(req).getServletContext(); + forwardRequestByPath(path, req, resp, sctx); + } + + /** + * Forwards the request from this resource to another resource at + * the JSP container level. This is a wrapper for + * PageContext.forward and PageContext.include + * and is intended to transparently switch between "forward" and + * "include" depending on whether or not an include has already been + * done on the request. + * + * @param path the URL of the resource, relative to the webapp + * root. For example, if you request a JSP page with /context/foo/bar, + * you would call this method with path == /foo/bar. + * + * @param pageContext the JSP page context + * + * @exception java.io.IOException may be propagated from target resource + * @exception javax.servlet.ServletException may be + * propagated from target resource + */ + public static void forwardRequestByPath(String path, + PageContext pageContext) + throws IOException, ServletException { + + ServletRequest req = pageContext.getRequest(); + Object attr = req.getAttribute(INCLUDE_URI); + // restore original request if we're using the wrapped + // multipart request here + if (attr != null) { + pageContext.include(path); + req.setAttribute(INCLUDE_URI, attr); + } else { + try { + pageContext.forward(path); + } catch (IllegalStateException e) { + pageContext.include(path); + } + } + } + + /** + * Forwards the request from this resource to a servlet resource + * (named in server.xml) at the servlet-container level. This is a + * wrapper for javax.servlet.RequestDispatcher and is intended to hide + * the fact that you can't "forward" a request once you've done an + * "include" in it. + * @param name the named servlet to forward to + * @param req the current request + * @param resp the current response + * @param sctx the current servlet context + * @exception java.io.IOException may be propagated from target resource + * @exception javax.servlet.ServletException may be + * propagated from target resource + */ + public static void forwardRequestByName(String name, + HttpServletRequest req, + HttpServletResponse resp, + ServletContext sctx) + throws IOException, ServletException { + RequestDispatcher rd = sctx.getNamedDispatcher(name); + forwardHelper(rd, req, resp); + } + + /** + * Equivalent to forwardRequestByName(name, req, resp, + * DispatcherHelper.getRequestContext(req).getServletContext()). + * + * @param name + * @param req + * @param resp + * @throws java.io.IOException + * @throws javax.servlet.ServletException + */ + public static void forwardRequestByName(String name, + HttpServletRequest req, + HttpServletResponse resp) + throws IOException, ServletException { + ServletContext sc = getRequestContext(req).getServletContext(); + forwardRequestByName(name, req, resp, sc); + } + + /** + * Given the name of a resource in the file system that is missing an + * extension, picks an extension that matches. Serves a file + * with a .jsp extension first, if available. + * Otherwise picks any file that matches. For directories, it tacks on + * the "index" filename plus the extension. + * + * Unsupported + * + * @param abstractFile the extensionless file + * @param actx the current application context + * @return a filename suffix (".jsp", "index.html", etc.) such + * that (abstractFile.getAbsolutePath() + suffix) is a valid file + * in the filesystem + * + * @exception com.arsdigita.dispatcher.RedirectException if the + * requested file is a directory and the original request URL does + * not end with a trailing slash. + * @exception java.io.FileNotFoundException if no matching + * file exists. + * @throws com.arsdigita.dispatcher.DirectoryListingException + * @deprecated abstract URLs are no longer supported. Use + * extensions when your file on disk has an extension. + */ + public static String resolveAbstractFile(File abstractFile, + RequestContext actx) + throws RedirectException, DirectoryListingException, + java.io.FileNotFoundException { + s_log.debug("Resolving abstract file"); + + File dirToSearch = null; + String fStr = abstractFile.getAbsolutePath(); + int lastSlash = fStr.lastIndexOf(File.separatorChar); + String filenameStub = fStr.substring(lastSlash + 1); + + boolean indexPage = false; + if (abstractFile.isDirectory()) { + if (!actx.getOriginalURL().endsWith("/")) { + // redirect to prevent confused browser + throw new RedirectException(actx.getOriginalURL() + "/"); + } + dirToSearch = abstractFile; + filenameStub = "index"; + indexPage = true; + } else if (abstractFile.exists()) { + // file exists and is not a directory; don't resolve any + // further + return ""; + } else { + dirToSearch = new File(abstractFile.getParent()); + } + + + File filesInDir[] = dirToSearch.listFiles(); + + final String extensionSearchList[] = {".jsp"}; + + if (filesInDir != null) { + for (String searchExtension : extensionSearchList) { //1.5 enhanced loop + File possibleFile = new File(dirToSearch, + filenameStub + searchExtension); + for (int i = 0; i < filesInDir.length; i++) { + if (filesInDir[i].equals(possibleFile)) { + return (indexPage ? File.separator + "index" : "") + + searchExtension; + } + } + } + + // no preferential matches, so just match whatever + // we can + // note that if we have an index page, we really don't + // want to match the abstractFile itself because it's + // a directory; we want to match the "index.*" file + // IN this directory. + File abstractFileIndex = new File(abstractFile, "index"); + for (int i = 0; i < filesInDir.length; i++) { + String fidStr = filesInDir[i].getPath(); + int lastDot = fidStr.lastIndexOf("."); + if (lastDot == -1) { + // Match must have extension + // perfect match already tested + continue; + } + File possibleStub = new File(fidStr.substring(0, lastDot)); + if (indexPage && abstractFileIndex.equals(possibleStub)) { + return "index" + fidStr.substring(lastDot); + } else if (abstractFile.equals(possibleStub)) { + return fidStr.substring(lastDot); + } + } + } + if (!abstractFile.isDirectory()) { + // couldn't find anything and not a directory, so throw + // An exception + throw new FileNotFoundException(abstractFile.getAbsolutePath()); + } else { + // we have a directory, no index file, maybe serve + // as a directory listing? + throw new DirectoryListingException(abstractFile.getAbsolutePath()); + } + } + + /** + * If the given servlet request is wrapped in one of our own classes, returns + * the original (unwrapped) request object and stores a reference + * to the request wrapper in the request attributes of the returned request. + * Otherwise just returns the request object. + * + * @param req the servlet request + * @return the original servlet request object, as created by + * the servlet container. This can be used as a parameter for forward(). + */ + public static HttpServletRequest restoreOriginalRequest(HttpServletRequest req) { + if (req instanceof MultipartHttpServletRequest) { + HttpServletRequest oldReq = (HttpServletRequest) req.getAttribute(ORIGINAL_REQUEST_ATTR); + oldReq.setAttribute(WRAPPED_REQUEST_ATTR, req); + req = oldReq; + } + return req; + } + + /** + * If we've stored a reference to a request wrapper as a request + * attribute to the current servlet request, returns the wrapper object. + * Otherwise, returns the request object. + * + * @param req the current servlet request + * @return the previously created wrapper around the current servlet + * request, if any; + * otherwise returns the request object. + */ + public static HttpServletRequest restoreRequestWrapper(HttpServletRequest req) { + // switch back wrapped request if we're forwarded + // from somewhere else. + Object maybeWrappedReq = req.getAttribute(WRAPPED_REQUEST_ATTR); + if (maybeWrappedReq != null + && !(req instanceof MultipartHttpServletRequest)) { + req = (HttpServletRequest) maybeWrappedReq; + } + return req; + } + + /** + * This method will optionally wrap the request if it is a multipart POST, + * or restore the original wrapper if it was already wrapped. + * + * @param sreq + * @return + * @throws java.io.IOException + * @throws javax.servlet.ServletException + */ + public static HttpServletRequest maybeWrapRequest(HttpServletRequest sreq) + throws IOException, ServletException { + final String type = sreq.getContentType(); + + if (sreq.getMethod().toUpperCase().equals("POST") + && type != null + && type.toLowerCase().startsWith("multipart")) { + final HttpServletRequest orig = sreq; + + final HttpServletRequest previous = + DispatcherHelper.restoreRequestWrapper(orig); + + if (previous instanceof MultipartHttpServletRequest) { + s_log.debug("Build new multipart request from previous " + + previous + " and current " + orig); + + MultipartHttpServletRequest previousmp = + (MultipartHttpServletRequest) previous; + + sreq = new MultipartHttpServletRequest(previousmp, + orig); + + DispatcherHelper.saveOriginalRequest(sreq, + orig); + + s_log.debug("The main request is now " + sreq); + } else { + s_log.debug("The request is a new multipart; wrapping the request " + + "object"); + try { + sreq = new MultipartHttpServletRequest(sreq); + } catch (MessagingException me) { + throw new ServletException(me); + } + + DispatcherHelper.saveOriginalRequest(sreq, orig); + } + } else { + s_log.debug("The request is not multipart; proceeding " + + "without wrapping the request"); + } + return sreq; + } + + /** + * Stores req as request attribute of oldReq. + * @param req the current servlet request (wrapper) + * @param oldReq the original servlet request + */ + public static void saveOriginalRequest(HttpServletRequest req, + HttpServletRequest oldReq) { + req.setAttribute(ORIGINAL_REQUEST_ATTR, oldReq); + } + + /** + * Redirects the client to the given URL without rewriting it. Delegates + * to the sendExternalRedirect method. + * + * @throws java.io.IOException + * @deprecated This method does not rewrite URLs. Use + * sendRedirect(HttpServletRequest, HttpServletResponse, String) for + * redirects within this ACS or + * sendExternalRedirect(HttpServletResponse, String) for redirects to + * sites outside this ACS. + * + * @param resp the current response + * @param url the destination URL for redirect + **/ + public static void sendRedirect(HttpServletResponse resp, + String url) + throws IOException { + sendExternalRedirect(resp, url); + } + + /** + * Rewrites the given URL and redirects the client to the rewritten URL. + * This method should be used for redirects within this ACS. + * + * @param req the current request; used as a source for parameters + * for URL rewriting + * @param resp the current response + * @param url the destination URL for redirect + * @throws java.io.IOException + **/ + public static void sendRedirect(HttpServletRequest req, + HttpServletResponse resp, + String url) + throws IOException { + sendExternalRedirect(resp, url); + } + + /** + * Redirects the client to the given URL without rewriting it. This + * method should be used for redirects to sites outside this ACS. + * + * @param resp the current response + * @param url the destination URL for redirect + * @throws java.io.IOException + **/ + public static void sendExternalRedirect(HttpServletResponse resp, + String url) + throws IOException { + if (s_log.isDebugEnabled()) { + s_log.debug("Redirecting to URL '" + url + "'", new Throwable()); + } + + if (StringUtils.emptyString(url)) { + // This is a fix so that redirecting to empty string + // (i.e. index file in current directory) + // works properly when running in Apache. + // DEE 3/13/01 the original apache redirect-fix string of "?" + // has been replaced with ".", because + // IE will reload the current page if redirected to "?". + url = "."; + } + + HttpServletRequest req = getRequest(); + Object attr; + if (req != null + && (attr = req.getAttribute(REENTRANCE_ATTRIBUTE)) != null) { + req.getSession(true).setAttribute(REDIRECT_SEMAPHORE, attr); + } + + if (url.startsWith("http")) { + final int start = url.indexOf("/", url.indexOf("//") + 2); + final String path = start >= 0 ? url.substring(start) : "/"; + + if (!path.startsWith(URL.getDispatcherPath())) { + url = path; + } + } + + if (url.startsWith("/")) { + final int sep = url.indexOf('?'); + URL destination = null; + + if (sep == -1) { + destination = URL.there(req, url); + if (s_log.isDebugEnabled()) { + s_log.debug("Setting destination to " + destination); + } + } else { + final ParameterMap params = ParameterMap.fromString(url.substring(sep + 1)); + + destination = URL.there(req, url.substring(0, sep), params); + if (s_log.isDebugEnabled()) { + s_log.debug("Setting destination with map to " + + destination); + } + } + throw new RedirectSignal(destination, true); + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("Redirecting to URL without using URL.there. " + + "URL is " + url); + } + throw new RedirectSignal(url, true); + } + } + + /** + * Adds a ParameterProvider to the URLRewriter engine. + * ParameterProviders are used when + * encodeRedirectURL and encodeURL are + * called. They add global state parameters like the session ID (for + * cookieless login) to URLs for links and redirects. + * + * @param provider the parameter provider to add + * @see com.arsdigita.util.URLRewriter#addParameterProvider + * @deprecated use URLRewriter.addParameterProvider + */ + public static void addParameterProvider(ParameterProvider provider) { + URLRewriter.addParameterProvider(provider); + } + + /** + * Clears all parameter providers. + * @deprecated use URLRewriter#clearParameterProviders() instead + * @see com.arsdigita.util.URLRewriter#clearParameterProviders() + **/ + public static void clearParameterProviders() { + URLRewriter.clearParameterProviders(); + } + + /** + * Returns the set of global parameter models, or the empty set if no + * provider is set. + * + * @return a set of Bebop parameter models. + * @deprecated use URLRewriter.getGlobalModels instead + * @see com.arsdigita.util.URLRewriter#getGlobalModels() + **/ + public static Set getGlobalModels() { + return URLRewriter.getGlobalModels(); + } + + /** + * Returns the set of global URL parameters for the given request, or + * the empty set if no provider is set. + * + * @param req the current request + * @return a Set of Bebop parameter data. + * @deprecated use URLRewriter.getGlobalParams instead + **/ + public static Set getGlobalParams(HttpServletRequest req) { + return URLRewriter.getGlobalParams(req); + } + + /** + * Prepares the given URL for a client link. If no providers are + * set, has no effect. + * + * @param url the target URL to prepare + * @return the prepared URL with global parameters added from + * providers + * + * @deprecated This method does not encode the servlet session ID. Use + * encodeURL(req, res, url) instead. + **/ + public static String prepareURL(String url, HttpServletRequest req) { + return URLRewriter.prepareURL(url, req); + } + + /** + * Encodes the given URL for the client. Adds ACS global parameters and + * servlet session parameters to the URL. If the URL will be used for + * redirection, use sendRedirect(req, resp, url) instead. + * + * @param req the current request + * @param resp the current response + * @param url the target URL (for a link) to encode + * @return the new URL, with extra URL variables added + * from parameter providers + * @deprecated use URLRewriter.encodeURL instead + * @see com.arsdigita.util.URLRewriter + **/ + public static String encodeURL(HttpServletRequest req, + HttpServletResponse resp, + String url) { + return URLRewriter.encodeURL(req, resp, url); + } + + /** + * Returns a global URL prefix for referencing static assets (images, CSS, + * etc.) on disk in href attributes. This can be on the same server + * ("/STATIC/") or a different server/port ("http://server:port/dir/"). + * The return value is guaranteed to end with a trailing slash. + * + * Usage example: + *
+     * String pathToImage = DispatcherHelper.getStaticURL() + "images/pic.gif";
+     * Image img = new Image(pathToImage);
+     * 
+ * + * @return a URL prefix ending with a trailing slash. + */ + public static String getStaticURL() { + init(); + return s_staticURL; + } + + /** + * sets the global URL prefix for referencing static assets (images, CSS, + * etc.) from user-agents in href attributes. + * Package visibility is intentional. + * + * @param s the static asset URL + * @pre s != null + */ + static void setStaticURL(String s) { + init(); + if (s == null) { + return; + } + if (!s.endsWith("/")) { + s += "/"; + } + s_staticURL = s; + } + + /** + * sets the webapp Context using the webappContext parameter in enterprise.init. + * This is an optional parameter. If it is not specified, null will be used and + * the value of s_webappContext will be set when there is a request. + * Package visibility is intentional. + * + * @param webappCtx the webappContext specified in enterprise.init. + * Normally this wouldbe "/". + */ + static void setWebappContext(String webappCtx) { + init(); + if (webappCtx == null) { + return; + } + if (!webappCtx.startsWith("/")) { + webappCtx = "/" + webappCtx; + } + s_webappCtx = webappCtx; + s_log.warn("webappContext set to '" + webappCtx + "'"); + } + + /** + * Gets the webapp Context using the following procedure: + * + * 1. If there is a request, get the value from the request. + * 2. If there is no request, get the value saved from a previous + * request. + * 3. If there is no request or previous request, use the value + * specified by the enterprise.init webappContext parameter. + * 4. Lastly, return null. + * + * @return + */ + public static String getWebappContext() { + init(); + String webappCtx = null; + HttpServletRequest request = DispatcherHelper.getRequest(); + if (request != null) { + webappCtx = request.getContextPath(); + + // Safety check to make sure the webappCtx from the request + // matches the webappCtx from enterprise.init. + if (s_webappCtx != null) { + if (s_webappCtx.equals("/")) { + s_webappCtx = ""; + } + if (!s_webappCtx.equals(webappCtx)) { + s_log.warn( + "webappContext changed. Expected='" + s_webappCtx + + "' found='" + webappCtx + "'.\nPerhaps the enterprise.init " + + "com.arsdigita.dispatcher.Initializer webappContext " + + "parameter is wrong."); + // Save the webappCtx from the request for future use. + s_webappCtx = webappCtx; + } + } + } else { + if (s_webappCtx != null && s_webappCtx.equals("/")) { + s_webappCtx = ""; + } + webappCtx = s_webappCtx; + } + return webappCtx; + } + + /** + * Aborts all processing of the current request and treat it + * as successfully completed. We abort the request by percolating + * an unchecked Error up through the call stack. Then the + * BaseDispatcherServlet.service method traps this and commits + * whatever DML has already happened on the transaction. + * + * @exception com.arsdigita.dispatcher.AbortRequestSignal Error thrown + * to abort current request + */ + public static void abortRequest() { + throw new AbortRequestSignal(); + } + + /** + * Stores the HttpServletRequest in a ThreadLocal so that it can be + * accessed globally. + * + * @param r + */ + public static void setRequest(HttpServletRequest r) { + init(); + s_request.set(r); + } + + /** + * Gets the current HttpServletRequest for this thread. + * @return the current HttpServletRequest for this thread. + */ + public static HttpServletRequest getRequest() { + init(); + return (HttpServletRequest) s_request.get(); + } + + /***************************************************/ + /* !!! Danger Will Robinson!!!! */ + /* */ + /* Don't go making changes to the cache headers */ + /* without reading *and* fully understanding the */ + /* sections on caching in RFC 2616 (HTTP 1.1) */ + /* */ + /* -- Daniel Berrange */ + /***************************************************/ + /** + * If no existing cache policy is set, then call + * cacheDisable to disable all caching of the response. + * @param response + */ + public static void maybeCacheDisable(HttpServletResponse response) { + if (!response.containsHeader("Cache-Control")) { + cacheDisable(response); + } + } + + /** + * Aggressively disable all caching of the response. + * + * @param response + */ + public static void cacheDisable(HttpServletResponse response) { + init(); + if (!s_cachingActive) { + return; + } + + // Assert.isTrue(!response.containsHeader("Cache-Control"), + // "Caching headers have already been set"); + // XXX Probably need to assert here if isCommitted() returns true. + // But first need to figure out what is setting Cache-Control. + if (response.containsHeader("Cache-Control")) { + s_log.warn("Cache-Control has already been set. Overwriting."); + } + + forceCacheDisable(response); + } + + /** + * + * @param response + */ + public static void forceCacheDisable(HttpServletResponse response) { + init(); + if (!s_cachingActive) { + return; + } + + s_log.info("Setting cache control to disable"); + // Aggressively defeat caching - works even for HTTP 0.9 proxies/clients! + response.setHeader("Pragma", "no-cache"); + response.setHeader("Cache-Control", "must-revalidate, no-cache"); + response.setHeader("Expires", rfc1123_formatter.format(new Date(0))); + } + + /** + * If no existing cache policy is set, then + * call cacheForUser to enable caching for a user. + * + * @param response + */ + public static void maybeCacheForUser(HttpServletResponse response) { + if (!response.containsHeader("Cache-Control")) { + cacheForUser(response); + } + } + + /** + * Allow caching of the response for this user only, as identified + * by the Cookie header. The response will expire according + * to the default age setting. + * + * @param response + */ + public static void cacheForUser(HttpServletResponse response) { + cacheForUser(response, s_defaultExpiry); + } + + /** + * If no existing cache policy is set, then + * call cacheForUser to enable caching for a user + * + * @param response + * @param maxage the max time in second until this expires + */ + public static void maybeCacheForUser(HttpServletResponse response, + int maxage) { + if (!response.containsHeader("Cache-Control")) { + cacheForUser(response, maxage); + } + } + + /** + * If no existing cache policy is set, then + * call cacheForUser to enable caching for a user + * + * @param response + * @param expiry the time at which to expire + */ + public static void maybeCacheForUser(HttpServletResponse response, + Date expiry) { + if (!response.containsHeader("Cache-Control")) { + cacheForUser(response, expiry); + } + } + + /** + * Allow caching of the response for this user only, + * as identified by the Cookie header. The response + * will expire in 'age' seconds time. + * @param response + * @param maxage the max life of the response in seconds + */ + public static void cacheForUser(HttpServletResponse response, + int maxage) { + init(); + if (!s_cachingActive) { + return; + } + + Assert.isTrue(!response.containsHeader("Cache-Control"), + "Caching headers have already been set"); + + s_log.info("Setting cache control to user"); + + // For HTTP/1.1 user agents, we tell them only cache + // for the original person making the request + response.setHeader("Last-Modified", rfc1123_formatter.format(new Date())); + response.setHeader("Cache-Control", "private, max-age=" + maxage); + + // NB. THis line is delibrately *NOT* using the actual expiry date + // supplied. HTTP/1.0 caches don't understand Cache-Control + // so we use a expiry time in the past to prevent accidental + // caching. HTTP/1.1 compliant caches still work, since they will + // look at the above max-age header in preference to Expires... + response.setHeader("Expires", rfc1123_formatter.format(new Date(0))); + } + + /** + * Allowing caching of the response for this user only. + * The response will expire at time given in the expiry parameter + * @param response + * @param expiry time at which to expire + */ + public static void cacheForUser(HttpServletResponse response, + Date expiry) { + cacheForUser(response, (int) ((expiry.getTime() - (new Date()).getTime()) / 1000l)); + } + + /** + * If no existing cache policy is set, then call cacheForUser to enable + * caching for the world. The response expiry will take the default + * age setting. + * + * @param response + */ + public static void maybeCacheForWorld(HttpServletResponse response) { + if (!response.containsHeader("Cache-Control")) { + cacheForWorld(response); + } + } + + /** + * Allow caching of this response for anyone in the world. + * The response take the default expiry time. + * + * @param response + */ + public static void cacheForWorld(HttpServletResponse response) { + cacheForWorld(response, s_defaultExpiry); + } + + /** + * If no existing cache policy is set, then call cacheForUser to enable + * caching for the world. + * + * @param response + * @param maxage the time in seconds until expiry + */ + public static void maybeCacheForWorld(HttpServletResponse response, + int maxage) { + if (!response.containsHeader("Cache-Control")) { + cacheForWorld(response, maxage); + } + } + + /** + * If no existing cache policy is set, then call cacheForUser to + * enable caching for the world. + * + * @param response + * @param expiry the time at which it will expire + */ + public static void maybeCacheForWorld(HttpServletResponse response, + Date expiry) { + if (!response.containsHeader("Cache-Control")) { + cacheForWorld(response, expiry); + } + } + + /** + * Allow caching of this response for anyone in the + * world. The response will expire at the current time + * plus maxage seconds. + * @param response + * @param maxage time in seconds until this expires + */ + public static void cacheForWorld(HttpServletResponse response, + int maxage) { + init(); + if (!s_cachingActive) { + return; + } + + Assert.isTrue(!response.containsHeader("Cache-Control"), + "Caching headers have already been set"); + + Calendar expires = Calendar.getInstance(); + expires.add(Calendar.SECOND, maxage); + + s_log.info("Setting cache control to world"); + response.setHeader("Cache-Control", "public, max-age=" + maxage); + response.setHeader("Expires", + rfc1123_formatter.format(expires.getTime())); + response.setHeader("Last-Modified", + rfc1123_formatter.format(new Date())); + } + + /** + * Allow caching of this response for anyone in the world. + * THe response will expire at the time given. + * + * @param response + * @param expiry + */ + public static void cacheForWorld(HttpServletResponse response, + Date expiry) { + cacheForWorld(response, (int) ((expiry.getTime() - (new Date()).getTime()) / 1000l)); + } + + /** + * This returns a reference to the dispatcher configuration file + * @return + */ + public static DispatcherConfig getConfig() { + if (s_config == null) { + s_config = new DispatcherConfig(); + s_config.load(); + } + return s_config; + } + + /** + * This method returns the best matching locale for the request. In contrast + * to the other methods available this one will also respect the + * supported_languages config entry. + * + * @return The negotiated locale + */ + public static Locale getNegotiatedLocale() { + KernelConfig kernelConfig = KernelConfig.getConfig(); + + // Set the preferedLocale to the default locale (first entry in the + // config parameter list) + Locale preferedLocale = new Locale(kernelConfig.getDefaultLanguage(), "", ""); + + // The ACCEPTED_LANGUAGES from the client + Enumeration locales = null; + + // Try to get the RequestContext + try { + locales = ((ServletRequest) DispatcherHelper.getRequest()).getLocales(); + + // For everey element in the enumerator + while (locales.hasMoreElements()) { + + // Test if the current locale is listed in the supported locales list + Locale curLocale = (Locale) locales.nextElement(); + if (kernelConfig.hasLanguage(curLocale.getLanguage())) { + preferedLocale = curLocale; + break; + } + + } + + } catch (NullPointerException ex) { + // Don't have to do anything because I want to fall back to default + // language anyway. This case should only appear during setup + } finally { + + return preferedLocale; + + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/dispatcher/InitialRequestContext.java b/ccm-core/src/main/java/com/arsdigita/dispatcher/InitialRequestContext.java new file mode 100644 index 000000000..dfaf39b19 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/dispatcher/InitialRequestContext.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.dispatcher; + +import java.util.Locale; +import java.util.ResourceBundle; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import org.apache.log4j.Logger; + +/** + * Implements a request context for the site map application + * or for any application dispatcher that creates the first application context + * for an incoming request. + * + * @author Bill Schneider + * @version $Id$ + * @since 4.5 + */ +public class InitialRequestContext implements RequestContext { + + private static final Logger s_log = Logger.getLogger + (InitialRequestContext.class); + + private String m_urlSoFar; + private String m_urlRemainder; + private String m_originalUrl; + private ServletContext m_sctx; + private String m_outputType; + private Locale m_locale; + private boolean m_debugging; //These three vars are for /debug, /xml, /xsl + private boolean m_debuggingXML; + private boolean m_debuggingXSL; + + /** + * Copy constructor. Creates a new + * InitialRequestContext with identical properties + * as the parameter that. This is useful for deferred + * construction of subclass objects with the same properties. + * + * @param that a request context to copy basic properties from. + * @post this.getProcessedURLPart() == that.getProcessedURLPart() + * @post this.getRemainingURLPart() == that.getRemainingURLPart() + * @post this.getOriginalURL() == that.getOriginalURL() + * @post this.getServletContext() == that.getServletContext() + * @post this.getLocale() == that.getLocale() + **/ + protected InitialRequestContext(RequestContext that) { + this.m_urlSoFar = that.getProcessedURLPart(); + this.m_urlRemainder = that.getRemainingURLPart(); + this.m_originalUrl = that.getOriginalURL(); + this.m_sctx = that.getServletContext(); + this.m_outputType = that.getOutputType(); + this.m_locale = that.getLocale(); + this.m_debugging = that.getDebugging(); + this.m_debuggingXML = that.getDebuggingXML(); + this.m_debuggingXSL = that.getDebuggingXSL(); + } + + /** + * Constructs a new request context from the given servlet + * request. Some initial URL portion has already been handled by + * the servlet container in dispatching to our web application. + * @param request the servlet request + * @param sctx the servlet context + */ + public InitialRequestContext(HttpServletRequest request, + ServletContext sctx) { + m_sctx = sctx; + initializeURLFromRequest(request, false); + + Object obj = request.getParameter("outputType"); + if (obj != null) { + m_outputType = (String)obj; + } else { + m_outputType = "text/html"; + } + + m_locale = request.getLocale(); + } + + /** + * Initializes the URL in this request context and decomposes + * it into a part already processed (what part of the URL got + * us here already?) and the part not yet processed (what + * part will the next dispatcher in the chain use?). + * In the initial step, the only part of the URL used so far + * is the part that selects the servlet context (webapp). + */ + void initializeURLFromRequest(HttpServletRequest request, + boolean preserveOriginalURL) { + s_log.debug("Initializing processed and remaining URL parts."); + + String requestUrl = DispatcherHelper.getCurrentResourcePath(request); + m_urlSoFar = request.getContextPath(); + m_urlRemainder = requestUrl; + + if (s_log.isDebugEnabled()) { + String contextPath = request.getContextPath(); + s_log.debug("contextPath: " + contextPath); + } + + if (s_log.isDebugEnabled()) { + String servletPath = request.getServletPath(); + s_log.debug("servletPath: " + servletPath); + } + + if (s_log.isDebugEnabled()) { + String pathInfo = request.getPathInfo(); + s_log.debug("pathInfo: " + pathInfo); + } + + final String debugURL = "/debug"; + m_debugging = m_urlRemainder.startsWith(debugURL); // humor JTest + if (m_debugging) { + m_urlSoFar += debugURL; + m_urlRemainder = m_urlRemainder.substring(debugURL.length()); + } + + final String debugURLXML = "/xml"; + m_debuggingXML = m_urlRemainder.startsWith(debugURLXML); // humor JTest + if (m_debuggingXML) { + m_urlSoFar += debugURLXML; + m_urlRemainder = m_urlRemainder.substring(debugURLXML.length()); + } + + final String debugURLXSL = "/xsl"; + m_debuggingXSL = m_urlRemainder.startsWith(debugURLXSL); // humor JTest + if (m_debuggingXSL) { + m_urlSoFar += debugURLXSL; + m_urlRemainder = m_urlRemainder.substring(debugURLXSL.length()); + } + if (!preserveOriginalURL) { + s_log.debug("Overwriting original URL, since the caller did not " + + "ask to preserve it"); + m_originalUrl = m_urlSoFar + m_urlRemainder; + } + + if (s_log.isDebugEnabled()) { + s_log.debug("Set processed URL to '" + m_urlSoFar + "'"); + s_log.debug("Set remaining URL to '" + m_urlRemainder + "'"); + } + } + + /** + * Returns the portion of the requested URL that was used so by far + * by all previous dispatchers in the chain. + * + * @return the portion of the requested URL that was used so by far + * by all previous dispatchers in the chain. + */ + public String getProcessedURLPart() { + return m_urlSoFar; + } + + /** + * Returns the portion of the requested URL that has not already been + * used by all previous dispatchers in the chain. + * + * @return the portion of the requested URL that has not already + * been used by all previous dispatchers in the chain. + */ + public String getRemainingURLPart() { + return m_urlRemainder; + } + + /** + * Sets the portion of the requested URL that has not already + * been processed by any previous dispatcher in the chain + * @param s the remaining unprocessed URL portion + */ + protected void setRemainingURLPart(String s) { + m_urlRemainder = s; + } + + /** + * Sets the portion of the requested URL that has already + * been processed by all the previous dispatchers in the chain. + * This allows decorating subclasses like SiteNodeRequestContext + * to mark an additional portion of the URL as processed. + * + * @param s the remaining unprocessed URL portion + */ + protected void setProcessedURLPart(String s) { + m_urlSoFar = s; + } + + public String getOriginalURL() { + return m_originalUrl; + } + + public ServletContext getServletContext() { + return m_sctx; + } + + /** + * At this point, we're not in any specific package, so just returns + * '/'. + */ + public String getPageBase() { + return "/"; + } + + /** + * @return the locale preferred by the user, as specified in the + * Accept-Language header. + */ + public Locale getLocale() { + return m_locale; + } + + public String getOutputType() { + return m_outputType; + } + + /** + * XXX Only added so that the class compiles. + */ + public ResourceBundle getResourceBundle() { + return null; + } + + public boolean getDebugging() { + return m_debugging; + } + + public boolean getDebuggingXML() { + return m_debuggingXML; + } + + public boolean getDebuggingXSL() { + return m_debuggingXSL; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/dispatcher/MultipartHttpServletRequest.java b/ccm-core/src/main/java/com/arsdigita/dispatcher/MultipartHttpServletRequest.java new file mode 100644 index 000000000..452661481 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/dispatcher/MultipartHttpServletRequest.java @@ -0,0 +1,603 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.dispatcher; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.mail.MessagingException; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletInputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.log4j.Category; + +import com.arsdigita.globalization.Globalization; +import com.arsdigita.util.UncheckedWrapperException; + +import java.util.Collection; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpUpgradeHandler; +import javax.servlet.http.Part; + +/** + * MultipartHttpServletRequest provides multipart/form-data handling + * capabilities to facilitate file uploads for servlets. This request object + * parses the HTTP request with MIME type "multipart" and places the encoded + * object in a stream. + * + * @author Karl Goldstein + * @author Michael Pih + * @author Uday Mathur + * @version $Id: MultipartHttpServletRequest.java 1512 2007-03-22 02:36:06Z + * apevec $ + * @since 4.5 + */ +public class MultipartHttpServletRequest implements HttpServletRequest { + + private static final Category s_log = Category.getInstance( + MultipartHttpServletRequest.class); + + private HttpServletRequest m_request; + private Map m_parameters = null; + + /** + * Create a multipart servlet request object and parse the request. + * + * @param request The request + * + * @throws javax.mail.MessagingException + * @throws java.io.IOException + */ + public MultipartHttpServletRequest(HttpServletRequest request) + throws MessagingException, IOException { + m_request = request; + m_parameters = Collections.synchronizedMap(new HashMap()); + parseMultipartRequest(m_request); + } + + /** + * Create a multipart servlet request object and parse the request. + * + * @param original + * @param current + */ + public MultipartHttpServletRequest(MultipartHttpServletRequest original, + HttpServletRequest current) { + m_request = current; + m_parameters = original.m_parameters; + } + + /** + * + * @param name + * + * @return + */ + @Override + public Object getAttribute(String name) { + return m_request.getAttribute(name); + } + + /** + * + * @return + */ + @Override + public Enumeration getAttributeNames() { + return m_request.getAttributeNames(); + } + + /** + * + * @param name + * + * @return + */ + @Override + public String getParameter(String name) { + String[] values = (String[]) m_parameters.get(name); + + if (values == null || values.length == 0) { + return null; + } else { + return values[0]; + } + } + + /** + * + * @return + */ + @Override + public Map getParameterMap() { + return m_parameters; + } + + /** + * + * @return + */ + @Override + public Enumeration getParameterNames() { + return Collections.enumeration(m_parameters.keySet()); + } + + /** + * + * @param name + * + * @return + */ + public String getFileName(String name) { + return getParameter(name); + } + + /** + * + * @param name + * + * @return + */ + public File getFile(String name) { + String path = getParameter(name + ".tmpfile"); + + if (path == null) { + return null; + } else { + return new File(path); + } + } + + /** + * + * @param name + * + * @return + */ + @Override + public String[] getParameterValues(String name) { + return (String[]) m_parameters.get(name); + } + + // Additional methods for HttpServletRequest + /** + * + * @return + */ + @Override + public String getAuthType() { + return m_request.getAuthType(); + } + + /** + * + * @return + */ + @Override + public Cookie[] getCookies() { + return m_request.getCookies(); + } + + public long getDateHeader(String name) { + return m_request.getDateHeader(name); + } + + public String getHeader(String name) { + return m_request.getHeader(name); + } + + public Enumeration getHeaders(String name) { + return m_request.getHeaders(name); + } + + public Enumeration getHeaderNames() { + return m_request.getHeaderNames(); + } + + public int getIntHeader(String name) { + return m_request.getIntHeader(name); + } + + public String getMethod() { + return m_request.getMethod(); + } + + public String getPathInfo() { + return m_request.getPathInfo(); + } + + public String getPathTranslated() { + return m_request.getPathTranslated(); + } + + public String getContextPath() { + return m_request.getContextPath(); + } + + public String getQueryString() { + return m_request.getQueryString(); + } + + public String getRemoteUser() { + return m_request.getRemoteUser(); + } + + public boolean isUserInRole(String role) { + return m_request.isUserInRole(role); + } + + public java.security.Principal getUserPrincipal() { + return m_request.getUserPrincipal(); + } + + public String getRequestedSessionId() { + return m_request.getRequestedSessionId(); + } + + public String getRequestURI() { + return m_request.getRequestURI(); + } + +// public StringBuffer getRequestURL() { +// throw new UnsupportedOperationException +// ("This is a Servlet 2.3 feature that we do not currently support"); +// } + + /* Obviously there was a problem with this method in early implementations + * of Servlet specification 2.3 which was resolved later. So it should be + * save to use it now. (2012-02-06) + */ + /** + * + * @return + */ + @Override + public StringBuffer getRequestURL() { + return m_request.getRequestURL(); + } + + /** + * + * @return + */ + @Override + public String getServletPath() { + return m_request.getServletPath(); + } + + public HttpSession getSession(boolean create) { + return m_request.getSession(create); + } + + public HttpSession getSession() { + return m_request.getSession(); + } + + public boolean isRequestedSessionIdValid() { + return m_request.isRequestedSessionIdValid(); + } + + public boolean isRequestedSessionIdFromCookie() { + return m_request.isRequestedSessionIdFromCookie(); + } + + public boolean isRequestedSessionIdFromURL() { + return m_request.isRequestedSessionIdFromURL(); + } + + public boolean isRequestedSessionIdFromUrl() { + return m_request.isRequestedSessionIdFromUrl(); + } + + //methods for ServletRequest Interface + public String getCharacterEncoding() { + return m_request.getCharacterEncoding(); + } + + /** + * + * @param encoding + * + * @throws java.io.UnsupportedEncodingException + */ + @Override + public void setCharacterEncoding(String encoding) + throws java.io.UnsupportedEncodingException { + throw new UnsupportedOperationException( + "This is a Servlet 2.3 feature that we do not currently support"); + } + + public int getContentLength() { + return m_request.getContentLength(); + } + + public String getContentType() { + return m_request.getContentType(); + } + + public ServletInputStream getInputStream() + throws IOException { + //maybe just throw an exception here -- UM + return m_request.getInputStream(); + } + + public String getProtocol() { + return m_request.getProtocol(); + } + + public String getScheme() { + return m_request.getScheme(); + } + + public String getServerName() { + return m_request.getServerName(); + } + + public int getServerPort() { + return m_request.getServerPort(); + } + + public BufferedReader getReader() throws IOException { + //maybe just throw an exception here -- Uday + return m_request.getReader(); + } + + public String getRemoteAddr() { + return m_request.getRemoteAddr(); + } + + public String getRemoteHost() { + return m_request.getRemoteHost(); + } + + public void setAttribute(String name, + Object o) { + m_request.setAttribute(name, o); + } + + public void removeAttribute(String name) { + m_request.removeAttribute(name); + } + + public Locale getLocale() { + return m_request.getLocale(); + } + + public Enumeration getLocales() { + return m_request.getLocales(); + } + + public boolean isSecure() { + return m_request.isSecure(); + } + + public RequestDispatcher getRequestDispatcher(String path) { + return m_request.getRequestDispatcher(path); + } + + public String getRealPath(String path) { + return m_request.getRealPath(path); + } + + /* + * Parse the body of multipart MIME-encoded request. + */ + private void parseMultipartRequest(HttpServletRequest request) + throws MessagingException, IOException { + // replace JAF+JavaMail combo (broken with CoyoteInputStream) + // with simple commons-fileupload + try { + ServletFileUpload upload = new ServletFileUpload( + new DiskFileItemFactory()); + List items = upload.parseRequest(request); + for (Iterator i = items.iterator(); i.hasNext();) { + FileItem item = (FileItem) i.next(); + String paramName = item.getFieldName(); + if (item.isFormField()) { + addParameterValue(paramName, item.getString()); + } else { + addParameterValue(paramName, item.getName()); + // save file + File tmpFile = File.createTempFile("acs", null, null); + tmpFile.deleteOnExit(); + addParameterValue(paramName + ".tmpfile", tmpFile.getPath()); + item.write(tmpFile); + } + } + } catch (Exception e) { + throw new RuntimeException(e); // XXX + } + } + + private void addParameterValue(String name, Object value) + throws IOException { + String[] newValues; + String[] values = (String[]) m_parameters.get(name); + + if (values == null) { + newValues = new String[1]; + } else { + newValues = new String[values.length + 1]; + System.arraycopy(values, 0, newValues, 0, values.length); + } + + newValues[newValues.length - 1] = convertToString(value); + + m_parameters.put(name, newValues); + } + + private String convertToString(Object value) throws IOException { + if (value instanceof String) { + return (String) value; + } + + if (value instanceof ByteArrayInputStream) { + StringBuilder output = new StringBuilder(); + + InputStreamReader reader; + try { + reader = new InputStreamReader( + (ByteArrayInputStream) value, + Globalization.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException ex) { + throw new UncheckedWrapperException(ex); + } + + final int bufSize = 1024; + char[] buffer = new char[bufSize]; + + int read = bufSize; + while (bufSize == read) { + read = reader.read(buffer, 0, bufSize); + if (read > 0) { + output.append(buffer, 0, read); + } + } + + return output.toString(); + } + + // Fallback to default + return value.toString(); + } + + public String getLocalAddr() { + return m_request.getLocalAddr(); + } + + public String getLocalName() { + // TODO Auto-generated method stub + return m_request.getLocalName(); + } + + public int getLocalPort() { + // TODO Auto-generated method stub + return m_request.getLocalPort(); + } + + public int getRemotePort() { + // TODO Auto-generated method stub + return m_request.getRemotePort(); + } + + @Override + public String changeSessionId() { + return m_request.changeSessionId(); + } + + @Override + public boolean authenticate(final HttpServletResponse response) + throws IOException, ServletException { + return m_request.authenticate(response); + } + + @Override + public void login(final String username, + final String password) throws ServletException { + m_request.login(username, password); + } + + @Override + public void logout() throws ServletException { + m_request.logout(); + } + + @Override + public Collection getParts() throws IOException, ServletException { + return m_request.getParts(); + } + + @Override + public Part getPart(final String name) throws IOException, ServletException { + return m_request.getPart(name); + } + + @Override + public T upgrade(final Class handlerClass) + throws IOException, ServletException { + return m_request.upgrade(handlerClass); + } + + @Override + public long getContentLengthLong() { + return m_request.getContentLengthLong(); + } + + @Override + public ServletContext getServletContext() { + return m_request.getServletContext(); + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return m_request.startAsync(); + } + + @Override + public AsyncContext startAsync(final ServletRequest servletRequest, + final ServletResponse servletResponse) + throws IllegalStateException { + return m_request.startAsync(servletRequest, servletResponse); + } + + @Override + public boolean isAsyncStarted() { + return m_request.isAsyncStarted(); + } + + @Override + public boolean isAsyncSupported() { + return m_request.isAsyncSupported(); + } + + @Override + public AsyncContext getAsyncContext() { + return m_request.getAsyncContext(); + } + + @Override + public DispatcherType getDispatcherType() { + return m_request.getDispatcherType(); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/dispatcher/RedirectException.java b/ccm-core/src/main/java/com/arsdigita/dispatcher/RedirectException.java new file mode 100644 index 000000000..3650fbdbd --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/dispatcher/RedirectException.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.dispatcher; + +/** + * Thrown if the requested file is a + * directory and the original request URL does not end with a trailing + * slash. + * + * @author Bill Schneider + * @version $Id$ + * @since 4.5 */ +public class RedirectException extends Exception { + + public RedirectException(String s) { + super(s); + } + + public RedirectException() { + super(); + } + + public String getRedirectURL() { + return getMessage(); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/dispatcher/RequestContext.java b/ccm-core/src/main/java/com/arsdigita/dispatcher/RequestContext.java new file mode 100644 index 000000000..2493e0dc2 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/dispatcher/RequestContext.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.dispatcher; + +import java.util.Locale; +import java.util.ResourceBundle; +import javax.servlet.ServletContext; + +/** + * Interface used when dispatchers are + * chained or piped together. Part of the requested URL will be used + * at each stage of the dispatch, and this interface is used by the + * dispatcher to tell what part of the URL has already been used to + * dispatch the request so far. The remainder is what the current + * dispatcher must work with. Because form/URL variables are not + * order-dependent, We only keep track of the path portion + * of the URL. + * + * @author Bill Schneider + * @version $Id$ + * @since 4.5 */ + +public interface RequestContext { + + + /** + * Gets the portion of the URL that has not been used by + * previous dispatchers in the chain. + * @return the portion of the URL that must be used by + * the current dispatcher. + */ + public String getRemainingURLPart(); + + /** + * Gets the portion of the URL that has already been used by + * previous dispatchers in the chain. + * @return the portion of the URL that has already been used. + */ + public String getProcessedURLPart(); + + /** + * Gets the original URL requested by the end user's browser. + * This URL does not change when a request is forwarded + * by the application; "/foo/bar" is still the original request + * URI in the browser even if we've dispatched the request to + * "/packages/foo/www/bar.jsp". + * + * @return the original URL requested by the end user's browser. + * All generated HREF, IMG SRC, and FORM ACTION attributes, and + * any redirects, will be relative to this URL. + */ + public String getOriginalURL(); + + /** + * Gets the current servlet context. + * @return the current servlet context, which must be set by implementation. + */ + public ServletContext getServletContext(); + + /** + * more methods will be implemented as needed, for locale, + * form variables, etc. + */ + + /** + * Gets the locale for the current request context. + * @return the locale for the current request context. + */ + public Locale getLocale(); + + /** + * Returns a java.util.ResourceBundle for the + * current request, based on the requested application and the + * user's locale preference. + * + * @return the current java.util.ResourceBundle to use + * in this request. + */ + public ResourceBundle getResourceBundle(); + + /** + * Gets the requested output type. + * @return the requested output type (normally "text/html" by default + * for a web browser request). + */ + public String getOutputType(); + + /** + * Gets the debugging flag. + * @return the debugging flag. + * Currently, debugging applies to XSL transformation. + */ + public boolean getDebugging(); + + /** + * Gets the show-XML-only flag. + * @return if true, indicates that the active + * PresentationManager should output raw, untransformed + * XML instead of processing it with XSLT. + */ + public boolean getDebuggingXML(); + + /** + * Gets the show-XSL-only flag. + * @return if true, indicates that the active + * PresentationManager should output the XSLT stylesheet + * in effect for this request. + */ + public boolean getDebuggingXSL(); + + + /** + * Gets the base path, relative to the webapp root, where JSP-based + * resources (and static pages) will be found. + * @return the base path, relative to the webapp root, where + * JSP-based resources will be found. + * Returns with a trailing slash (for example, + * /packages/package-key/www/). + */ + public String getPageBase(); +} diff --git a/ccm-core/src/main/java/com/arsdigita/globalization/Globalization.java b/ccm-core/src/main/java/com/arsdigita/globalization/Globalization.java new file mode 100644 index 000000000..c2eb63a21 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/globalization/Globalization.java @@ -0,0 +1,542 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.globalization; + +import com.arsdigita.dispatcher.DispatcherHelper; +import com.arsdigita.dispatcher.RequestContext; +//import com.arsdigita.kernel.Kernel; +//import com.arsdigita.persistence.DataCollection; +//import com.arsdigita.persistence.Session; +//import com.arsdigita.persistence.SessionManager; +//import com.arsdigita.persistence.TransactionContext; +import com.arsdigita.util.Assert; +import java.io.UnsupportedEncodingException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import javax.servlet.http.HttpServletRequest; +import org.apache.log4j.Logger; + +/** + *

+ * Utilities for the globalization process. The methods in this class make + * use of the assumption that the ACS handles all locale and resource + * negotiation so that the application developer doesn't have to worry about + * it. + *

+ * + * @version $Id$ + */ +public class Globalization { + + private static final Logger s_log = Logger.getLogger(Globalization.class); + + public static final String ENCODING_PARAM_NAME = "g11n.enc"; + + /** + * The default encoding for parameterts, as specified by the + * servlet spec + */ + public static final String DEFAULT_PARAM_ENCODING = "ISO-8859-1"; + + /** + * The default encoding for request/response body data, as specified by the + * servlet spec + */ + public static final String DEFAULT_ENCODING = "ISO-8859-1"; + +// private static Map s_localeToCharsetMap; + + private static String s_defaultCharset = DEFAULT_ENCODING; + + private static boolean initialized = false; + + static void init() { + if (initialized) { + return; + } +// loadLocaleToCharsetMap(); + initialized = true; + } + + +// // Load the Locale to Charset Map from persistent storage. +// public static void loadLocaleToCharsetMap() { +// // retrieve all Locale objects that have a defaultCharset associated +// // with them. +// Session s = SessionManager.getSession(); +// +// final TransactionContext tcontext = s.getTransactionContext(); +// boolean startedTransaction = false; +// +// try { +// if (!tcontext.inTxn()) { +// tcontext.beginTxn(); +// startedTransaction = true; +// } +// +// DataCollection locales = s.retrieve(Locale.BASE_DATA_OBJECT_TYPE); +// locales.addNotEqualsFilter("defaultCharset.id", null); +// +// Map map = new HashMap(); +// +// while (locales.next()) { +// Locale localeObject = new Locale(locales.getDataObject()); +// java.util.Locale locale = localeObject.toJavaLocale(); +// Charset defaultCharset = localeObject.getDefaultCharset(); +// Assert.exists(defaultCharset, +// "DefaultCharset for locale \"" +// + locale + "\" (" + localeObject + ")"); +// String charset = defaultCharset.getCharset(); +// +// if (s_log.isInfoEnabled()) { +// s_log.info("Mapping locale " + locale.toString() + +// " to charset " + charset); +// } +// +// // insert locale and charset into map +// map.put(locale.toString(), charset); +// } +// +// s_localeToCharsetMap = map; +// } finally { +// if (startedTransaction && tcontext.inTxn()) { +// tcontext.commitTxn(); +// } +// } +// } + + static void setDefaultCharset(String encoding) { + s_defaultCharset = encoding; + } + + /** + * Get the default character set for encoding data + * + * @return String the character set + */ + public static String getDefaultCharset() { + return s_defaultCharset; + } + + /** + *

+ * Get the default character set for a given locale. + *

+ * + * @param locale + * + * @return String the character set + * + * @see java.util.Locale + */ + public static String getDefaultCharset(java.util.Locale locale) { + init(); + return "UTF-8"; +// String charset; +// +// if (locale == null || locale.toString().length() == 0) { +// throw new IllegalArgumentException("locale cannot be empty."); +// } +// +// if (s_log.isDebugEnabled()) { +// s_log.debug("Looking for charset for locale " + locale.toString()); +// } +// // Try a full name match (may include country) +// charset = "UTF-8"; +// +// if (charset != null) { +// // Found a match +// return charset; +// } +// +// if (s_log.isDebugEnabled()) { +// s_log.debug("Looking for charset for language " + locale.getLanguage()); +// } +// // If we didn't find a full name match, try just the language +// charset = "UTF-8"; +// +// if ( charset != null ) { +// return charset; +// } +// +// if (s_log.isDebugEnabled()) { +// s_log.debug("Falling back on default encoding " + getDefaultCharset()); +// } +// return getDefaultCharset(); + } + + /** + * Get the default character set for the request. First + * tries the getCharacterENcoding() method, then falls + * back on the DEFAULT_PARAM_ENCODING + * + * @return String the character set + */ + public static String getDefaultCharset(HttpServletRequest req) { + String charset = req.getCharacterEncoding(); + if (charset == null) { + charset = DEFAULT_PARAM_ENCODING; + } + return charset; + } + + /** + * Get the best locale for this request. + */ + private static java.util.Locale getLocale(HttpServletRequest req) { + java.util.Locale l = DispatcherHelper.getNegotiatedLocale(); + if (l == null) { + l = req.getLocale(); + } + if (l == null) { + l = java.util.Locale.getDefault(); + } + return l; + } + + /** + *

+ * Decode the value of an HttpServletRequest parameter. The value is + * decoded appropriately (lets hope so anyway). + *

+ * + * @param r The HttpServletRequest for which to get the value. + * @param name The name of the parameter to retrieve. + * + * @return String The decoded value of the parameter. + */ + public static final String decodeParameter( + HttpServletRequest r, String name + ) { + String re = r.getParameter(Globalization.ENCODING_PARAM_NAME); + String original = r.getParameter(name); + String real = null; + + if (re == null || + re.length() == 0) { + if (s_log.isDebugEnabled()) { + s_log.debug(ENCODING_PARAM_NAME + " is not set, using locale " + + "default encoding for parameter " + name); + } + re = getDefaultCharset(getLocale(r)); + } + + if (original == null || + original.length() == 0) { + if (s_log.isDebugEnabled()) { + s_log.debug("Parameter " + name + " has no value"); + } + real = original; + } else if (getDefaultCharset(r).equals(re)) { + if (s_log.isDebugEnabled()) { + s_log.debug("Parameter " + name + " is already in correct encoding"); + } + real = original; + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("Parameter " + name + " is being converted from " + + getDefaultCharset(r) + " into " + re); + } + try { + real = new String + (original.getBytes(getDefaultCharset(r)), + re); + } catch (UnsupportedEncodingException uee) { + s_log.warn("encoding " + re + " is not supported, falling back on system default"); + real = original; + } + } + + return real; + } + + /** + *

+ * Decode all of the values of an HttpServletRequest array parameter. + *

+ * + * @param r The HttpServletRequest for which to decode the parameters. + * + * @return String[] The decoded parameters. + */ + public static final String[] decodeParameters + (HttpServletRequest r, String name) { + String re = r.getParameter(Globalization.ENCODING_PARAM_NAME); + String[] originals = r.getParameterValues(name); + String[] real = null; + + if (re == null || + re.length() == 0) { + if (s_log.isDebugEnabled()) { + s_log.debug(ENCODING_PARAM_NAME + " is not set, using locale " + + "default encoding for parameter " + name); + } + re = getDefaultCharset(getLocale(r)); + } + + if (originals == null || + originals.length == 0) { + if (s_log.isDebugEnabled()) { + s_log.debug("Parameter " + name + " has no value"); + } + real = originals; + } else if (getDefaultCharset(r).equals(re)) { + if (s_log.isDebugEnabled()) { + s_log.debug("Parameter " + name + " is already in correct encoding"); + } + real = originals; + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("Parameter " + name + " is being converted from " + + getDefaultCharset(r) + " into " + re); + } + try { + real = new String[originals.length]; + for (int i = 0; i < originals.length; i++) { + real[i] = new String + (originals[i].getBytes(getDefaultCharset(r)), + re); + } + } catch (UnsupportedEncodingException uee) { + s_log.warn("encoding " + re + " is not supported, falling back on system default"); + real = originals; + } + } + + return real; + } + + /** + *

+ * Get the appropriate ResourceBundle based ont he request and locale. + *

+ * + * @return ResourceBundle + * + * @see java.util.ResourceBundle + */ + public static ResourceBundle getResourceBundle() { + return getResourceBundle(DispatcherHelper.getRequest()); + } + + /** + *

+ * Get the appropriate ResourceBundle based on the request and Locale + *

+ * + * @param r The current HttpServletRequest + * + * @return ResourceBundle + * + * @see java.util.ResourceBundle + */ + public static ResourceBundle getResourceBundle(HttpServletRequest r) { + RequestContext rc = DispatcherHelper.getRequestContext(r); + ResourceBundle rb = null; + + rb = rc.getResourceBundle(); + + if (rb != null) { + if (s_log.isInfoEnabled()) { + s_log.info(rb.getClass().getName() + + " is the chosen ResourceBundle."); + } + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("No matching ResourceBundle found"); + } + } + + return rb; + } + + /** + *

+ * Get an Object from the appropriate ResourceBundle based on the + * appropriate Locale and key. + *

+ * + * @param r The current HttpServletRequest. + * @param key The key used to select the appropriate Object + * + * @return The localized Object + * + * @see java.util.ResourceBundle + */ + public static Object getLocalizedObject(HttpServletRequest r, + String key) { + ResourceBundle rb = null; + Object l7dObject = key; + + // If the key does not contain a '#' character, then use the + // HttpServletRequest alone to determine the appropriate + // ResourceBundle. + + int separator = key.indexOf('#'); + if (separator < 0) { + rb = getResourceBundle(r); + } else { + java.util.Locale locale = DispatcherHelper.getNegotiatedLocale(); + String targetBundle = key.substring(0, separator); + + try { + if (locale != null) { + rb = ResourceBundle.getBundle(targetBundle, locale); + } else { + rb = ResourceBundle.getBundle(targetBundle); + } + } catch (MissingResourceException mre) { + return key; + } + + key = key.substring(separator + 1); + } + + try { + if (rb != null) { + l7dObject = rb.getObject(key); + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("No ResourceBundle available"); + } + } + } catch (MissingResourceException e) { + if (s_log.isDebugEnabled()) { + s_log.debug("Key " + key + " was not found in the " + + "ResourceBundle"); + } + } + + return l7dObject; + } + + /** + *

+ * Get a String from the appropriate ResourceBundle based on the + * appropriate Locale and key. + *

+ * + * @param r The current HttpServletRequest. + * @param key The key used to select the appropriate String + * + * @return The localized String + * + * @see java.util.ResourceBundle + */ + public static String getLocalizedString(HttpServletRequest r, + String key) { + return (String) getLocalizedObject(r, key); + } + + /** + *

+ * Get a parameterized String (for doing MessageFormatting) from the + * appropraite ResourceBundle based on the appropriate Locale and key. + * Then interpolate the values for the other keys passed. + *

+ * + * @param r The current HttpServletRequest. + * @param key The key used to select the appropriate String + * @param arguments A Object[] containing the other keys to localize and + * interpolate into the parameterized string. It may also + * contain other Objects beside Strings, such as Date + * objects and Integers, etc. + * + * @return The localized and interpolated String + * + * @see java.text.MessageFormat + * @see java.util.ResourceBundle + */ + public static String getLocalizedString(HttpServletRequest r, + String key, + Object[] arguments) { + String l7dString = getLocalizedString(r, key); + + for (int i = 0; i < arguments.length; i++) { + // if we encounter a String object then treat it as a key and try + // to look it up in the appropriate ResourceBundle. + if (arguments[i] instanceof String) { + arguments[i] = getLocalizedString(r, (String) arguments[i]); + } + } + + // interpolate the values into the final string. + l7dString = MessageFormat.format(l7dString, arguments); + + return l7dString; + } + + /** + *

+ * Find the ResourceBundle for this language without falling back to a + * default ResourceBundle in another language + *

+ * + * @param targetBundle The ResourceBundle we are looking for. + * @param locale The Locale object representing the language we want. + * @param defaultLocale The Locale object representing the default language. + */ + public static ResourceBundle getBundleNoFallback + (String targetBundle, java.util.Locale locale, + java.util.Locale defaultLocale) { + ResourceBundle bundle = null; + + if (locale == null) { + locale = + (defaultLocale != null) ? + defaultLocale : java.util.Locale.getDefault(); + } + + try { + bundle = ResourceBundle.getBundle(targetBundle, locale); + } catch (MissingResourceException e) { + if (s_log.isInfoEnabled()) { + s_log.info("Didn't find ResourceBundle for " + targetBundle); + } + } + + String targetLanguage = locale.getLanguage(); + + // Make sure that if we found a ResourceBundle it is either in the + // language we were looking for or, by coincidence, the target + // language happens to match the default language for the system. + if (bundle != null ) { + if ( + targetLanguage.equals(bundle.getLocale().getLanguage()) || + targetLanguage.equals(defaultLocale.getLanguage()) + ) { + if (s_log.isInfoEnabled()) { + s_log.info("Found matching ResourceBundle for " + + targetBundle); + } + } else { + if (s_log.isInfoEnabled()) { + s_log.info("Found non-matching ResourceBundle for " + + targetBundle); + } + bundle = null; + } + } + + return bundle; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/globalization/GlobalizationHelper.java b/ccm-core/src/main/java/com/arsdigita/globalization/GlobalizationHelper.java new file mode 100644 index 000000000..1d5783b56 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/globalization/GlobalizationHelper.java @@ -0,0 +1,147 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.arsdigita.globalization; + +import com.arsdigita.dispatcher.DispatcherHelper; +import com.arsdigita.kernel.KernelConfig; +import java.util.Enumeration; +import javax.servlet.ServletRequest; +import java.util.Locale; +import java.util.StringTokenizer; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +/** + * + * @author Sören Bernstein + */ +public class GlobalizationHelper { + + public static final String LANG_INDEPENDENT = KernelConfig.getConfig().getLanguagesIndependentCode(); + private static final String LANG_PARAM = "lang"; + + // Don't instantiate + private GlobalizationHelper() { + } + + /** + * This method returns the best matching locate for the request. In contrast to + * the other methods available this one will also respect the supported_languages + * config entry. + * + * @return The negotiated locale + */ + public static java.util.Locale getNegotiatedLocale() { + KernelConfig kernelConfig = KernelConfig.getConfig(); + + // Set the preferedLocale to the default locale (first entry in the config parameter list) + java.util.Locale preferedLocale = getPrefferedLocale(); + + // The ACCEPTED_LANGUAGES from the client + Enumeration locales = null; + + // Try to get the RequestContext + try { + + // Get the SerrvletRequest + ServletRequest request = ((ServletRequest) DispatcherHelper.getRequest()); + + // Get the selected locale from the request, if any + java.util.Locale selectedLocale = getSelectedLocale(request); + if (selectedLocale != null && kernelConfig.hasLanguage(selectedLocale.getLanguage())) { + preferedLocale = selectedLocale; + } else { + + locales = request.getLocales(); + + // For everey element in the enumerator + while (locales.hasMoreElements()) { + + // Test if the current locale is listed in the supported locales list + java.util.Locale curLocale = (Locale) locales.nextElement(); + if (kernelConfig.hasLanguage(curLocale.getLanguage())) { + preferedLocale = curLocale; + break; + } + } + } + } catch (NullPointerException ex) { + // Don't have to do anything because I want to fall back to default language anyway + // This case should only appear during setup + } finally { + + return preferedLocale; + + } + } + +// public static java.util.Locale getSystemLocale() { +// +// } + private static Locale getPrefferedLocale() { + KernelConfig kernelConfig = KernelConfig.getConfig(); + java.util.Locale preferedLocale = new java.util.Locale(kernelConfig.getDefaultLanguage(), "", ""); + return preferedLocale; + } + + /** + * Get the selected (as in fixed) locale from the ServletRequest + * + * @return the selected locale as java.util.Locale or null if not defined + */ + public static Locale getSelectedLocale(ServletRequest request) { + + // Return value + java.util.Locale selectedLocale = null; + + // Access current HttpSession or create a new one, if none exist + HttpSession session = ((HttpServletRequest) request).getSession(true); + // Get the session stored language string + String selectedSessionLang = (String) session.getAttribute(LANG_PARAM); + // Get the request langauge string + String selectedRequestLang = request.getParameter(LANG_PARAM); + + // If there is a request language string, then this will have priority + // because this will only be the case, if someone selected another + // language with the language selector + if(selectedRequestLang != null) { + // Get the Locale object for the param + if((selectedLocale = scanLocale(selectedRequestLang)) != null) { + // Save the request parameter as session value + session.setAttribute(LANG_PARAM, selectedRequestLang); + } + } else { + // If there is a session stored language, use it + if(selectedSessionLang != null) { + selectedLocale = scanLocale(selectedSessionLang); + } + } + + return selectedLocale; + } + + /** + * Create a Locale from a browser provides language string + * + * @param lang A string encoded locale, as provided by browsers + * @return A java.util.Locale representation of the language string + */ + private static java.util.Locale scanLocale(String lang) { + + // Protect against empty lang string + if ((lang != null) && !(lang.isEmpty())) { + // Split the string and create the Locale object + StringTokenizer paramValues = new StringTokenizer(lang, "_"); + if (paramValues.countTokens() > 1) { + return new java.util.Locale(paramValues.nextToken(), paramValues.nextToken()); + } else { + return new java.util.Locale(paramValues.nextToken()); + } + } + + return null; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/globalization/Globalized.java b/ccm-core/src/main/java/com/arsdigita/globalization/Globalized.java new file mode 100644 index 000000000..3eb15cb60 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/globalization/Globalized.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.globalization; +// import java.text.DateFormat; + +/** + * Standard, final constants used by the globalization APIs. This + * interface is designed to be extended on a package-by-package basis + * to include package-specific constants. A typical package specific + * constant would be the resource bundle name for a given package. By + * extending the Globalized interface and defining a final static + * BUNDLE_NAME constant, classes implementing the Globalized interface + * could call {@link GlobalizedMessage} and pass in the BUNDLE_NAME + * constant i.e., + * + *

+ *

+ *  new GlobalizedMessage("forums.newpost.proofread",
+ *                        BUNDLE_NAME)
+ * 
+ *

+ * + * @version $Revision$ $Date$ + * @version $Id$ + */ +public interface Globalized { + + /** The default format for displaying dates. */ + public final static int DATE_DISPLAY_FORMAT = java.text.DateFormat.MEDIUM; + + /** The default format for displaying time. */ + public final static int TIME_DISPLAY_FORMAT = java.text.DateFormat.SHORT; + + /** Override the value of this string for your particular package. */ + public final static String BUNDLE_NAME = "com.arsdigita.globalization"; + +} diff --git a/ccm-core/src/main/java/com/arsdigita/globalization/GlobalizedMessage.java b/ccm-core/src/main/java/com/arsdigita/globalization/GlobalizedMessage.java new file mode 100644 index 000000000..fa99ffe7e --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/globalization/GlobalizedMessage.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.globalization; + +import java.text.MessageFormat; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import javax.servlet.http.HttpServletRequest; +import org.apache.log4j.Logger; + +/** + *

+ * Represents a key into a ResourceBundle, a target ResourceBundle, and possibly an array of + * arguments to interpolate into the retrieved message using the MessageFormat class. + *

+ *

+ * This class should be used in any situation where the application needs to output localizeable + * objects. + *

+ * + * @see java.text.MessageFormat + * @see java.util.Locale + * @see java.util.ResourceBundle + * + * @version $Id$ + */ +public class GlobalizedMessage { + + /** + * Internal logger instance to faciliate debugging. Enable logging output by editing + * /WEB-INF/conf/log4j.properties int hte runtime environment and set + * com.arsdigita.globalization.GlobalizedMessage=DEBUG by uncommenting or adding the line. + */ + private static final Logger LOGGER = Logger.getLogger(GlobalizedMessage.class.getName()); + private String m_key = ""; + private String m_bundleName = ""; + /** + * {@link ResourceBundle.Control} used by this {@code GlobalizedMessage} for looking up + * the ResourceBundle. Defaults to {@link ResourceBundle.Control#getNoFallbackControl(java.util.List)} + * to avoid that the locale of the server is taken into account. + */ + private ResourceBundle.Control rbControl = ResourceBundle.Control.getNoFallbackControl( + ResourceBundle.Control.FORMAT_DEFAULT); + private Object[] m_args = null; + + /** + *

+ * Constructor. Takes in a key to be used to look up a message in the ResourceBundle for the + * current running application. The base name of the ResourceBundle to do the lookup in is + * retrieved from the ApplicationContext. + *

+ * + * @param key The key to use to look up a message in the ResourceBundle. + */ + public GlobalizedMessage(final String key) { + setKey(key); + setBundleName(); + } + + /** + *

+ * Constructor. Takes in a key to be used to look up a message in the ResourceBundle specified. + *

+ * + * @param key The key to use to look up a message in the ResourceBundle. + * @param bundleName The base name of the target ResourceBundle. + */ + public GlobalizedMessage(final String key, final String bundleName) { + setKey(key); + setBundleName(bundleName); + } + + /** + *

+ * Constructor. Takes in a key to be used to look up a message in the ResourceBundle for the + * current running application. The base name of the ResourceBundle to do the lookup in is + * retrieved from the ApplicationContext. Also takes in an Object[] of arguments to interpolate + * into the retrieved message using the MessageFormat class. + *

+ * + * @param key The key to use to look up a message in the ResourceBundle. + * @param args An Object[] of arguments to interpolate into the retrieved message. + */ + public GlobalizedMessage(final String key, final Object[] args) { + this(key); + setArgs(args); + } + + /** + *

+ * Constructor. Takes in a key to be used to look up a message in the ResourceBundle specified. + * Also takes in an Object[] of arguments to interpolate into the retrieved message using the + * MessageFormat class. + *

+ * + * @param key The key to use to look up a message in the ResourceBundle. + * @param bundleName The base name of the target ResourceBundle. + * @param args An Object[] of arguments to interpolate into the retrieved message. + */ + public GlobalizedMessage(final String key, + final String bundleName, + final Object[] args) { + this(key, bundleName); + setArgs(args); + } + + public GlobalizedMessage(final String key, + final ResourceBundle.Control rbControl) { + this(key); + this.rbControl = rbControl; + } + + public GlobalizedMessage(final String key, + final Object[] args, + final ResourceBundle.Control rbControl) { + this(key, args); + this.rbControl = rbControl; + } + + public GlobalizedMessage(final String key, + final String bundleName, + final ResourceBundle.Control rbControl) { + this(key, bundleName); + this.rbControl = rbControl; + } + + public GlobalizedMessage(final String key, + final String bundleName, + final Object[] args, + final ResourceBundle.Control rbControl) { + this(key, bundleName, args); + this.rbControl = rbControl; + } + + /** + *

+ * Get the key for this GlobalizedMessage. + *

+ * + * @return String The key for this GlobalizedMessage. + */ + public final String getKey() { + return m_key; + } + + /** + * + * @param key + */ + private void setKey(final String key) { + if (key == null || key.length() == 0) { + throw new IllegalArgumentException("key cannot be empty."); + } + + m_key = key; + } + + private String getBundleName() { + return m_bundleName; + } + + private void setBundleName() { + // setBundleName(ApplicationContext.get().getTargetBundle()); + setBundleName("com.arsdigita.dummy.DummyResources"); + } + + private void setBundleName(final String bundleName) { + if (bundleName == null || bundleName.length() == 0) { + throw new IllegalArgumentException("bundleName cannot be empty."); + } + + m_bundleName = bundleName; + } + + private void setArgs(final Object[] args) { + m_args = args; + } + + /** + *

+ * Localize this message. If no message is found the key is returned as the message. This is + * done so that developers or translators can see the messages that still need localization. + *

+ *

+ * Any arguments this message has are interpolated into it using the java.text.MessageFormat + * class. + *

+ * + * @return Object Represents the localized version of this message. The reason this method + * returns an Object and not a String is because we might want to localize resources other than + * strings, such as icons or sound bites. Maybe this class should have been called + * GlobalizedObject? + */ + public Object localize() { + return localize(com.arsdigita.globalization.GlobalizationHelper + .getNegotiatedLocale()); + } + + /** + *

+ * Localize this message according the specified request. If no message is found the key is + * returned as the message. This is done so that developers or translators can see the messages + * that still need localization. + *

+ *

+ * Any arguments this message has are interpolated into it using the java.text.MessageFormat + * class. + *

+ * + * @param request The current running request. + * + * @return Object Represents the localized version of this message. The reason this method + * returns an Object and not a String is because we might want to localize resources other than + * strings, such as icons or sound bites. Maybe this class should have been called + * GlobalizedObject? + */ + public Object localize(final HttpServletRequest request) { + return localize(com.arsdigita.globalization.GlobalizationHelper + .getNegotiatedLocale()); + } + + /** + *

+ * Localize this message with the provided locale. If no message is found the key is returned as + * the message. This is done so that developers or translators can see the messages that still + * need localization. + *

+ *

+ * Any arguments this message has are interpolated into it using the java.text.MessageFormat + * class. + *

+ * + * @param locale The locale to try to use to localize this message. + * + * @return Object Represents the localized version of this message. The reason this method + * returns an Object and not a String is because we might want to localize resources other than + * strings, such as icons or sound bites. Maybe this class should have been called + * GlobalizedObject? + */ + public Object localize(final Locale locale) { + Object message = getKey(); + ResourceBundle resourceBundle = null; + + if (locale == null) { + throw new IllegalArgumentException("locale cannot be null."); + } + + try { + // jensp 2013-03-16: + // Previously, ResourceBundle#getBundle(String, Locale) was called here. That was causing problems under + // specific circumstances: + // - The browser of the user is set the english (britain), languge code en_GB + // - The system language of the server running CCM is set to german (de_DE). + // In this case, the ResourceBundle.getBundle method first tries to find a ResourceBundle for en_GB, than + // for en. Usally, both attempts will fail because the english labels are in the default bundle + // (no language code). The standard search algorithm of ResourceBundle#getBundle than falls back to the + // system language (the language of the SERVER), which is German is this case. Therefore the content center + // was shown with german texts... + // Luckily, there is a simple solution: The search algorithm is implemented in the inner class + // ResourceBundle.Control. There are also variants of the getBundle method which allow it to pass an + // custom implementation of ResouceBundle.Control. Also ResourceBundle.Control has a factory method which + // offers an implementation of ResourceBundle.Control which does not use the system language. + // Therefore, all what was to do was to change the call of getBundle here from + // ResourceBundle#getBundle(String, Locale) to ResourceBundle#getBundle(String, Locale, ResourceControl) + // with ResourceBundle.Control.getNoFallbackControl(List). + // jensp 2014-07-10: + // It is now possible to pass the custom implementation of ResourceBundle.Control to + // a GlobalizedMessage + resourceBundle = ResourceBundle.getBundle( + getBundleName(), + locale, + rbControl); + } catch (MissingResourceException e) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "ResourceBundle " + getBundleName() + " was not found."); + } + } + + try { + if (resourceBundle == null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("No ResourceBundle available"); + } + } else { + message = resourceBundle.getObject(getKey()); + } + } catch (MissingResourceException e2) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(getKey() + " was not found in the ResourceBundle."); + } + } + + if (m_args != null && m_args.length > 0 && message instanceof String) { + Object[] args = new Object[m_args.length]; + System.arraycopy(m_args, 0, args, 0, m_args.length); + + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof GlobalizedMessage) { + args[i] = ((GlobalizedMessage) args[i]).localize(locale); + } + } + + message = MessageFormat.format((String) message, args); + } + + return message; + } + + /** + * For debugging, not for localizing. + * + * If you need a String, use an additional localize() to get an object and cast it to String. + * e.g. String label = (String) GlobalizedMessage(key,bundleName).localize(); + * + * @return The contents in String form for debugging. + */ + @Override + public String toString() { + return getBundleName() + "#" + getKey(); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/ApplicationOIDPatternGenerator.java b/ccm-core/src/main/java/com/arsdigita/templating/ApplicationOIDPatternGenerator.java new file mode 100755 index 000000000..f0f8efd1e --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/ApplicationOIDPatternGenerator.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + + +import com.arsdigita.web.Web; + +import org.libreccm.web.Application; + +import javax.servlet.http.HttpServletRequest; + +import java.net.URLEncoder; + +/** + * This looks for the current application and will return its OID if + * it is available + */ +public class ApplicationOIDPatternGenerator implements PatternGenerator { + + /** + * Looks up the current application and returns its OID as String. + * The Return type is (unneccessarily) String[] due to the current API, but + * currently never returns more than just one value (one OID). + * + * @param key placeholder from the pattern string, without surrounding + * colons, constantly "oid" here. + * @param req current HttpServletRequest + * @return OID as String in an Array of Strings. This array is never longer + * as one element. + */ + @Override + public String[] generateValues(String key, + HttpServletRequest req) { + + final Application application = Web.getWebContext().getApplication(); + + if (application != null) { + String[] oid = new String[1]; + // FR: better URLEncode this + oid[0] = URLEncoder.encode(Long.toString(application.getObjectId())); + return oid; + } else { + return new String[] {}; + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/ApplicationPatternGenerator.java b/ccm-core/src/main/java/com/arsdigita/templating/ApplicationPatternGenerator.java new file mode 100755 index 000000000..9d9613643 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/ApplicationPatternGenerator.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +// import com.arsdigita.dispatcher.DispatcherHelper; +// import com.arsdigita.sitenode.SiteNodeRequestContext; +// import com.arsdigita.kernel.SiteNode; +import com.arsdigita.web.Web; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.log4j.Logger; +import org.libreccm.web.Application; +import org.libreccm.web.ApplicationType; + +/** + * Generates a set of pattern values based on the application key, eg + * content-center, content-section. + */ +public class ApplicationPatternGenerator implements PatternGenerator { + + /** + * Private logger instance for debugging purpose + */ + private static final Logger s_log = Logger.getLogger(PatternGenerator.class); + + /** + * Implementation iof the Interface class. + * + * @param key + * @param req + * + * @return + */ + @Override + public String[] generateValues(String key, + HttpServletRequest req) { + + s_log.debug("Processing Application with key: " + key); + + final Application app = Web.getWebContext().getApplication(); + if (app != null) { + String[] returnValue = {((ApplicationType) app.getResourceType()) + .getTitle()}; + s_log.debug("Found application >>" + returnValue + + "<< in Application."); + return returnValue; + } + + s_log.debug("ApplicationType for >>" + key + + "<< not found. Trying SiteNodes instead."); + + throw new IllegalArgumentException( + "No ApplicationType found for type name " + key); + + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/HostPatternGenerator.java b/ccm-core/src/main/java/com/arsdigita/templating/HostPatternGenerator.java new file mode 100755 index 000000000..aed3c1114 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/HostPatternGenerator.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import com.arsdigita.util.servlet.HttpHost; +import com.arsdigita.web.Web; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.log4j.Logger; + + +/** + * Generates a set of patterns corresponding to the current + * host name. (actually just retrieves the current hostname from configuration + * file, StringArray returned is for sake of methods consistency) + */ +public class HostPatternGenerator implements PatternGenerator { + + /** Internal logger instance to faciliate debugging. Enable logging output + * by editing /WEB-INF/conf/log4j.properties int the runtime environment + * and set com.arsdigita.templating.HostPatternGenerator=DEBUG + * by uncommenting or adding the line. */ + private static final Logger s_log = + Logger.getLogger(HostPatternGenerator.class); + + /** + * Looks up the hostname from configuration and returns it as String. + * The Return type is (unneccessarily) String[] due to the current API, but + * currently never returns more than just one value (one hostname:port). + * + * @param key placeholder from the pattern string, without surrounding + * colons, constantly "host" here. + * @param req current HttpServletRequest + * @return Hostname (including port if any), retrieved from CCM + * configuration + */ + @Override + public String[] generateValues(String key, + HttpServletRequest req) { + HttpHost host = Web.getConfig().getHost(); + String hostName = host.toString(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Generating Values for key: " + key + " [" + + "Hostname retrieved: >>" + hostName + "<<]"); + } + + return new String[] { host.toString() }; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/LocalePatternGenerator.java b/ccm-core/src/main/java/com/arsdigita/templating/LocalePatternGenerator.java new file mode 100755 index 000000000..e1a9cb879 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/LocalePatternGenerator.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import com.arsdigita.dispatcher.DispatcherHelper; + +import javax.servlet.http.HttpServletRequest; + +/** + * Generates a pattern based on the request negotiated locale + * in com.arsdigita.kernel.KernelContext + */ +public class LocalePatternGenerator implements PatternGenerator { + public String[] generateValues(String key, + HttpServletRequest req) { + return new String[] { + DispatcherHelper.getNegotiatedLocale().toString() + }; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/OutputTypePatternGenerator.java b/ccm-core/src/main/java/com/arsdigita/templating/OutputTypePatternGenerator.java new file mode 100755 index 000000000..68797bf33 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/OutputTypePatternGenerator.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import javax.servlet.http.HttpServletRequest; + +/** + * Generates a pattern for based on the outputType request + * parameter + */ +public class OutputTypePatternGenerator implements PatternGenerator { + public String[] generateValues(String key, + HttpServletRequest req) { + String query = req.getQueryString(); + if (query != null) { + int typeIndex = query.indexOf("outputType"); + if (typeIndex > -1) { + int secondaryIndex = query.indexOf("&", typeIndex); + String type = null; + if (secondaryIndex > -1) { + type = query.substring(typeIndex, secondaryIndex); + } else { + type = query.substring(typeIndex); + } + type = type.toLowerCase(); + if (type.indexOf("text/javascript") > -1) { + return new String[] { "text-javascript" }; + } else if (type.indexOf("text/html") > -1) { + return new String[] { "text-html" }; + } else if (type.indexOf("text/plain") > -1) { + return new String[] { "text-plain" }; + } + } + } + + return new String[] { }; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/PatternGenerator.java b/ccm-core/src/main/java/com/arsdigita/templating/PatternGenerator.java new file mode 100755 index 000000000..4b45dac28 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/PatternGenerator.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import javax.servlet.http.HttpServletRequest; + + +public interface PatternGenerator { + + public String[] generateValues(String name, + HttpServletRequest request); +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/PatternStylesheetResolver.java b/ccm-core/src/main/java/com/arsdigita/templating/PatternStylesheetResolver.java new file mode 100755 index 000000000..6c69f93d3 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/PatternStylesheetResolver.java @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import com.arsdigita.util.StringUtils; +import com.arsdigita.util.UncheckedWrapperException; + +import java.io.LineNumberReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.FileNotFoundException; +import java.io.IOException; + +import java.net.URL; +import java.net.MalformedURLException; + +import java.util.HashMap; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.LinkedList; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.log4j.Logger; + +/** + *

+ * This stylesheet resolver is used by the *PresentationManager + * class to work out which XSLT stylesheet to apply to the current Bebop + * XML output. + *

+ * + *

+ * This particular stylesheet resolver uses a flat file containing a list + * of stylesheet patterns, one per line. The file is called + * WEB-INF/resources/stylesheet-paths.txt. + * Such a file could look like this: + *

+ * + *
+ * # Comments and empty lines are ignored.
+ *
+ * /packages/aplaws/xsl/::vhost::/cms_::locale::.xsl
+ * /packages/aplaws/xsl/::vhost::/cms.xsl
+ * /packages/aplaws/xsl/default/cms_::locale::.xsl
+ * /packages/aplaws/xsl/default/cms.xsl
+ * /packages/content-section/xsl/cms_::locale::.xsl
+ * /packages/content-section/xsl/cms.xsl
+ * 
+ * + *

+ * You may use the com.arsdigita.templating.stylesheet_paths system + * property to change the file from which the stylesheet patterns are drawn. + *

+ * + *

+ * The patterns, such as ::vhost::, are substituted + * for string values: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * clear + * + * + * + * + * + *
Pattern Meaning Examples
::host:: Host name www.aplaws.org
::vhost:: Virtual hostname. business.camden.gov.uk
::webapp:: Current web application name (ie. context or document root) ccm
::application:: Current CCM Application name navigation
::url:: URL stub of the current applications name tree admin
::prefix:: ?? ??
::outputtype:: Output format. text_html
::locale:: Current locale fr_FR
::outputtype:: Output format. text_html
+ * + *

+ * Each substituted string is cleaned up using the following rules: + *

+ * + *
    + *
  • Whitespace is trimmed. + *
  • Converted to lowercase + *
  • If the string is null, it is converted to "default". + *
  • Any "/" characters are converted to "_" (underscore). + *
+ * + *

+ * The resolver looks at each stylesheet in turn, and the first one which + * actually exists on disk is returned. + *

+ * + * Developer may customize the process by writing a custom pattern generator + * and add it in a custom integration package Initializer (e.g. ccm-ldn-aplaws) + * by following code: + * // Register additional PatternStyleSheetResolver for Web app. + * PatternStylesheetResolver.registerPatternGenerator( + * "[myKey]", + * new [My]PatternGenerator() + * ); + * + * + * @author Richard W.M. Jones + */ +public class PatternStylesheetResolver implements StylesheetResolver { + + /** Internal logger instance to faciliate debugging. Enable logging output + * by editing /WEB-INF/conf/log4j.properties int the runtime environment + * and set com.arsdigita.templating.PatternStylesheetResolver=DEBUG + * by uncommenting or adding the line. */ + private static final Logger s_log = Logger.getLogger + (PatternStylesheetResolver.class); + + /** List of registered pattern generators which are queried in turn. */ + private static final HashMap s_generators = new HashMap(); + + + /** + * Registers a new pattern generator for the given key. + * + * @param key the key as it appears in the pattern string + * @param gen a pattern generator for producing values to be + * substituted for key + */ + public static void registerPatternGenerator(String key, + PatternGenerator gen) { + s_generators.put(key, gen); + } + + /* Statiic initializer block to initialize the standard pattern generators + * at load time. */ + static { + s_log.debug("Static initalizer starting..."); + registerPatternGenerator + ("locale", new LocalePatternGenerator()); + registerPatternGenerator + ("url", new URLPatternGenerator()); + registerPatternGenerator + ("application", new ApplicationPatternGenerator()); + registerPatternGenerator + ("outputtype", new OutputTypePatternGenerator()); + registerPatternGenerator + ("prefix", new PrefixPatternGenerator()); + registerPatternGenerator + ("webapp", new WebAppPatternGenerator()); + registerPatternGenerator + ("host", new HostPatternGenerator()); + s_log.debug("Static initalizer finished."); + } + + /** Complete path to the file specifing stylesheet patterns. Configurable + * by configuration option in TemplatingConfig */ + private String m_path = null; + /** A List of Lists each of its lists containing one pattern to resolve + * a probably appropriate stylesheet to apply. (i.e. one row of the + * file m_path above) */ + private List m_paths = null; + + /** + * + * @param request + * @return + */ + @Override + public URL resolve(HttpServletRequest request) { + synchronized(this) { + if (m_paths == null) { + loadPaths(Templating.getConfig().getStylesheetPaths()); + } + } + s_log.debug("m_paths is " + m_paths); + + HashMap values = new HashMap(); + ArrayList paths = new ArrayList(); + Iterator it = m_paths.iterator(); + while (it.hasNext()) { + List pathList = (List) it.next(); + String[] bits = (String[])pathList.toArray( + new String[pathList.size()] + ); + expandPlaceholders(bits, paths, values, request); + } + + Iterator files = paths.iterator(); + while (files.hasNext()) { + String[] bits = (String[])files.next(); + + String resource = StringUtils.join(bits, ""); + // UGLY HACK + // If a placeholder returns an empty string (as in the example of + // the root webapp) the provided string contains a "//" as there is + // a slash before as well as after the placeholder in the pattern + // string. It's ugly so we'll replace it. + resource = resource.replace("//","/"); + // The hack destroys the http protocol as well so we need another hack + resource = resource.replace("http:/","http://"); + + if (s_log.isInfoEnabled()) { + s_log.info("Looking to see if resource " + resource + " exists"); + } + + URL origURL = null; + try { + origURL = new URL(resource); + } catch (MalformedURLException ex) { + throw new UncheckedWrapperException( + "malformed URL " + resource, ex); + } + + if (s_log.isInfoEnabled()) { + s_log.info("origURL is " + origURL); + } + + final URL xfrmedURL = (origURL==null) ? null : Templating + .transformURL(origURL); + + if (s_log.isInfoEnabled()) { + s_log.info("Transformed resource is " + xfrmedURL); + } + + try { + InputStream is = null; + if (xfrmedURL != null) { + is = xfrmedURL.openStream(); + } + if (is != null) { + is.close(); + // xfrmedURL may test for existence either as http request + // or as a file lookup. Anyway we return the original URL + // which used to be a http request. + // Note: we are returning the URL, not the resource! + return origURL; + } + } catch (FileNotFoundException ex) { + if (s_log.isDebugEnabled()) { + s_log.debug("File not found " + resource, ex); + } + // fall through & try next pattern + } catch (IOException ex) { + throw new UncheckedWrapperException("cannot open stream " + + resource, ex); + } + } + + throw new RuntimeException("no path to XSL stylesheet found; " + + "try modifying " + m_path); + } + + /** + * + * @param inBits + * @param paths + * @param values + * @param request + */ + private void expandPlaceholders(String[] inBits, + ArrayList paths, + HashMap values, + HttpServletRequest request) { + LinkedList queue = new LinkedList(); + if (s_log.isDebugEnabled()) { + s_log.debug("Queue initial entry " + StringUtils.join(inBits, "")); + } + queue.add(inBits); + while (!queue.isEmpty()) { + String[] bits = (String[])queue.removeFirst(); + if (s_log.isDebugEnabled()) { + s_log.debug("Process queue entry " + StringUtils.join(bits, "")); + } + boolean clean = true; + for (int i = 0 ; i < bits.length && clean ; i++) { + if (bits[i].startsWith("::") && bits[i].endsWith("::")) { + clean = false; + String[] vals = getValues(bits[i] + .substring(2, bits[i].length()-2), + values, + request); + if (vals != null) { + for (int k = 0 ; k < vals.length ; k++) { + String[] newBits = new String[bits.length]; + // In case the pattern for an element is an empty + // string (e.g. for the ROOT webapp) the slash before + // as well as after the placeholder are added + // resulting in a "//" which does no harm but is + // ugly. + for (int j = 0 ; j < bits.length ; j++) { + if (j == i) { + newBits[j] = vals[k]; + } else { + newBits[j] = bits[j]; + } + } + if (s_log.isDebugEnabled()) { + s_log.debug("Requeue " + + StringUtils.join(newBits, "")); + } + queue.add(newBits); + } + } + } + } + + if (clean) { + if (s_log.isDebugEnabled()) { + s_log.debug("Finished expanding placeholders in " + + StringUtils.join(bits, "")); + } + paths.add(bits); + } + } + } + + /** + * + * @param key + * @param values + * @param request + * @return + */ + private String[] getValues(String key, + HashMap values, + HttpServletRequest request) { + if (s_log.isDebugEnabled()) { + s_log.debug("Lookup placeholder keys for " + key); + } + String[] vals = (String[])values.get(key); + if (vals == null) { + PatternGenerator gen = (PatternGenerator) s_generators.get(key); + if (gen == null) { + return new String[] {}; + } + vals = gen.generateValues(key, request); + values.put(key, vals); + } + return vals; + } + + /** + * + * @param path + */ + private void loadPaths(String path) { + if (s_log.isInfoEnabled()) { + s_log.info("Loading paths from " + path); + } + + m_path = path; + try { + // Read the source file. + ClassLoader cload = Thread.currentThread().getContextClassLoader(); + InputStream stream = cload.getResourceAsStream(path.substring(1)); + s_log.debug("got stream using path " + path.substring(1)); + s_log.debug("stream.available is " + stream.available()); + m_paths = new ArrayList(); + + LineNumberReader file = new LineNumberReader + (new InputStreamReader(stream)); + String line; + int lineNum; + while ((line = file.readLine()) != null) { + lineNum = file.getLineNumber(); + // Ignore blank lines and comments. + line = line.trim(); + s_log.debug("line is " + line); + if ("".equals(line) || line.startsWith("#") + || line.startsWith("!") || line.startsWith("//")) { + continue; + } + + // Split up the line. + List list = StringUtils.splitUp(line, "/::\\w+::/"); + // Save the split line. + m_paths.add(list); + } + } catch (IOException ex) { + throw new UncheckedWrapperException( + "cannot read XSLT paths from " + path, ex); + + } catch (Exception e) { + s_log.debug("loadPaths threw exception " + e); + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/PrefixPatternGenerator.java b/ccm-core/src/main/java/com/arsdigita/templating/PrefixPatternGenerator.java new file mode 100755 index 000000000..d4ef430d8 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/PrefixPatternGenerator.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import com.arsdigita.dispatcher.DispatcherHelper; +import javax.servlet.http.HttpServletRequest; + +/** + * Generates a pattern for based on the request dispatcher prefix, + * eg /print/content/myitem.jsp -> { 'print' } + * /text-only/content/myitem.jsp -> { 'text-only' } + */ +public class PrefixPatternGenerator implements PatternGenerator { + + /** + * + * @param key + * @param req + * @return + */ + @Override + public String[] generateValues(String key, + HttpServletRequest req) { + String value = DispatcherHelper.getDispatcherPrefix(req); + if (value != null) { + return new String[] { value.substring(1) }; + } + else { + return new String[] { }; + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/PresentationManager.java b/ccm-core/src/main/java/com/arsdigita/templating/PresentationManager.java new file mode 100755 index 000000000..8ab8bd3cc --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/PresentationManager.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import com.arsdigita.xml.Document; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Interface for styling and serving XML documents to the response output + * stream. + * + * The PresentationManager contains the code that determines which + * XSLT transformer(s) are to be applied to a given document. + * + * The (default) SimplePresentationManager just links to the bebop + * implementation. It should suffice for most cases. + * + * A custom presentation manager is needed if an application needs to + * dynamically apply a set of templates to an XML document in a custom + * way. Typically, this occurs if the template selection + * depends on the outcome of some application-specific logic. + * + * @see com.arsdigita.templating.SimplePresentationManager + * + * @author Bill Schneider + * @version ACS 4.6 + * @version $Id$ + */ +public interface PresentationManager { + + /** + * Serves a page whose content is defined by the input XML + * document. Gets an appropriate XSLT Transformer object and + * uses the transformer to convert the DOM input to the final + * output. + * + * @param doc the XML document whose content is to be displayed + * to the output + * @param req the servlet request + * @param resp the servlet response + */ + public void servePage(Document doc, + HttpServletRequest req, + HttpServletResponse resp); + + // WRS: I really would like to be able to define + // "public static getInstance()" here to make the singleton pattern + // enforced at compile time, but Java doesn't allow that declaration + // in an interface or abstract class. +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/SimplePresentationManager.java b/ccm-core/src/main/java/com/arsdigita/templating/SimplePresentationManager.java new file mode 100755 index 000000000..5bd65ef20 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/SimplePresentationManager.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +/** + * An Implementation of a Presentation Manager as specified by the + * {@link PresentationManager} interface which may be used as a default. + * + * As bebop is currently the only one providing a presenation layer it simply + * links to the bebop implementation. At the same time it makes shure an + * implementation exists which can be used as default in the templating + * configuration registry. + */ +/* NON Javadoc comment: + * Used to be deprecated in version 6.6.0. Reverted to non-deprecated in version + * 6.6.0 release 3. Package templating provides the basic mechanism for CCM + * templating system an should provide an implementation of the Presentation + * Manager interface to be complete. + * @ deprecated Use {@link com.arsdigita.bebop.page.PageTransformer} + * instead + */ +public class SimplePresentationManager + extends com.arsdigita.bebop.page.PageTransformer + implements PresentationManager{ + // Empty +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/SimpleURIResolver.java b/ccm-core/src/main/java/com/arsdigita/templating/SimpleURIResolver.java new file mode 100755 index 000000000..c3711c3e0 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/SimpleURIResolver.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; +import javax.xml.transform.stream.StreamSource; + +import org.apache.log4j.Logger; + +/** + * An implementation of the URIResolver interface that keeps track of all the + * URLs that have been loaded. + * If you set this as the URI resolver for a Transformer then this will track + * all the xsl:import and xsl:include statements. + * + * @version $Id$ + */ +final class SimpleURIResolver implements URIResolver { + + private static final Logger s_log = Logger.getLogger + (SimpleURIResolver.class); + + private final Set m_uniqueStylesheetURIs; + private final List m_stylesheetURIs; + + /** + * Constructor, just initializes internal properties. + */ + public SimpleURIResolver() { + m_uniqueStylesheetURIs = new HashSet(); + m_stylesheetURIs = new ArrayList(); + } + + /** + * Returns all the stylesheet URIs encountered so far. + * + * @return a Set whose elements are isntances of java.net.URL + */ + public List getStylesheetURIs() { + return m_stylesheetURIs; + } + + /** + * Resolves a URL and returns a stream source. + * + * @param href the url to resolve + * @param base the base url to resolve relative to + */ + @Override + public Source resolve(final String href, final String base) + throws TransformerException { + if (s_log.isDebugEnabled()) { + s_log.debug("Resolve " + href + " (found in " + base + ")"); + } + + URL baseURL = null; + + if (base != null) { + try { + baseURL = new URL(base); + } catch (MalformedURLException ex) { + throw new TransformerException("cannot parse href " + base, ex); + } + } + + URL thisURL = null; + + try { + if (baseURL == null) { + thisURL = new URL(href); + } else { + thisURL = new URL(baseURL, href); + } + + if (!m_uniqueStylesheetURIs.contains(thisURL)) { + m_uniqueStylesheetURIs.add(thisURL); + m_stylesheetURIs.add(thisURL); + } + } catch (MalformedURLException ex) { + throw new TransformerException("cannot parse href " + href, ex); + } + + try { + if (s_log.isDebugEnabled()) { + s_log.debug("Got url " + thisURL); + } + + // Optimize calls to resource servlet into file:/// + // where possible + URL xfrmedURL = Templating.transformURL(thisURL); + + if ( xfrmedURL == null ) { + throw new TransformerException + ("URL does not exist: " + thisURL); + } + + if (s_log.isInfoEnabled()) { + s_log.info("Loading URL " + xfrmedURL); + } + + InputStream is = xfrmedURL.openStream(); + + // NB, don't pass through 'xfrmedURL' since imports + // are relative to 'thisURL' + return new StreamSource(is, thisURL.toString()); + } catch (IOException ex) { + throw new TransformerException( + String.format("cannot read stream for %s", + thisURL.toString()), + ex); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/StylesheetResolver.java b/ccm-core/src/main/java/com/arsdigita/templating/StylesheetResolver.java new file mode 100755 index 000000000..999ec51d6 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/StylesheetResolver.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import java.net.URL; +import javax.servlet.http.HttpServletRequest; + +/** + * The various PresentationManager classes resolve requests + * into stylesheets using classes derived from this interface. + * + * @version $Id$ + */ +public interface StylesheetResolver { + + /** + * Resolves a template for the request. + * + * @param sreq the HttpServletRequest for which to + * resolve a template + * @return URL where to try to find a stylesheet + */ + public URL resolve(HttpServletRequest sreq); +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/Templating.java b/ccm-core/src/main/java/com/arsdigita/templating/Templating.java new file mode 100755 index 000000000..17ee373e2 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/Templating.java @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.arsdigita.templating; + +import com.arsdigita.bebop.Bebop; +import com.arsdigita.kernel.KernelConfig; +import com.arsdigita.util.Assert; +import com.arsdigita.util.ExceptionUnwrapper; +import com.arsdigita.util.Exceptions; +import com.arsdigita.util.UncheckedWrapperException; +import com.arsdigita.util.servlet.HttpHost; +import com.arsdigita.web.Web; +import com.arsdigita.xml.Document; +import com.arsdigita.xml.Element; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +import javax.servlet.http.HttpServletRequest; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.ErrorListener; +import javax.xml.transform.TransformerException; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +/** + * An entry-point class for the functions of the templating package. The class + * manages access to all theme files (XSL as well as css, pirctures, etc). + * + * This class maintains a cache of XSLTemplate objects, managed + * via the getTemplate and purgeTemplate methods. + * + * @author Dan Berrange + * @author Justin Ross <jross@redhat.com> + * @version $Id$ + */ +public class Templating { + + /** Internal logger instance to faciliate debugging. Enable logging output + * by editing /WEB-INF/conf/log4j.properties int hte runtime environment + * and set com.arsdigita.templating.Templating=DEBUG by uncommenting it + * or adding the line. */ + private static final Logger s_log = Logger.getLogger(Templating.class); + + /** This is the name of the attribute that is set in the request whose + * value, if present, is a collection of TransformerExceptions that + * can be used to produce a "pretty" error. */ + public static final String FANCY_ERROR_COLLECTION = "fancyErrorCollection"; + + + /** Config object containing various parameter */ + private static final TemplatingConfig s_config = TemplatingConfig + .getInstanceOf(); + + static { + s_log.debug("Static initalizer starting..."); + + Exceptions.registerUnwrapper( + TransformerException.class, + new ExceptionUnwrapper() { + + @Override + public Throwable unwrap(Throwable t) { + TransformerException ex = (TransformerException) t; + return ex.getCause(); + } + }); + + // now we initiate the CacheTable here + + // default cache size used to be 50, which is too high I reckon, + // each template can eat up to 4 megs + Integer setting = s_config.getCacheSize(); + int cacheSize = (setting == null ? 10 : setting.intValue()); + + setting = s_config.getCacheAge(); + int cacheAge = (setting == null ? 60 * 60 * 24 * 3 : setting.intValue()); + + s_log.debug("Static initalizer finished..."); + } + + /** + * Gets the TemplatingConfig record. + * + * @return The TemplatingConfig of this runtime + */ + public static TemplatingConfig getConfig() { + return s_config; + } + + /** + * Returns a new instance of the current presentation manager class. This is + * an object which has the + * {@link com.arsdigita.templating.PresentationManager PresentationManager} + * interface which can be used to transform an XML document into an output + * stream. + * + * As of version 6.6.0 the bebop framework is the only instance to provide + * an implementation. To avoid class hierachie kludge we directly return the + * bebop config here. + * + * @return an instance of the PresentationManager + * interface + */ + /* NON Javadoc + * Used to be deprecated up to version 6.6.0. Reverted to non-deprecated. + * Package templating provides the basic mechanism for CCM templating + * machinerie and provides the Presentation Manager interface. It should be + * able to be queried for an implementation as well. + * @ deprecated Use {@link + * com.arsdigita.bebop.BebopConfig#getPresentationManager()} + * instead. + */ + public static PresentationManager getPresentationManager() { + return Bebop.getConfig().getPresentationManager(); + } + + /** + * Retrieves an XSL template. If the template is already loaded in the + * cache, it will be returned. If the template has been modified since + * it was first generated, it will be regenerated first. + * + * @param source the URL to the top-level template resource + * @return an XSLTemplate instance representing + * source + */ + public static synchronized XSLTemplate getTemplate(final URL source) { + return getTemplate(source, false, true); + } + + /** + * Retrieves an XSL template. If the template is already loaded in the + * cache, it will be returned. If the template has been modified since + * it was first generated, it will be regenerated first. + * + * @param source the URL to the top-level template resource + * @param fancyErrors Should this place any xsl errors in the request + * for use by another class. If this is true, the + * the errors are stored for later use. + * @param useCache Should the templates be pulled from cache, if available? + * True means they are pulled from cache. False means + * they are pulled from the disk. If this is false + * the pages are also not placed in the cache. + * @return an XSLTemplate instance representing + * source + */ + public static synchronized XSLTemplate getTemplate(final URL source, + boolean fancyErrors, + boolean useCache) { + + if (s_log.isDebugEnabled()) { + s_log.debug("Getting template for URL " + source); + } + + Assert.exists(source, URL.class); + + XSLTemplate template = null; + + if (template == null) { + if (s_log.isInfoEnabled()) { + s_log.info("The template for URL " + source + " is not " + + "cached; creating and caching it now"); + } + + if (fancyErrors) { + LoggingErrorListener listener = new LoggingErrorListener(); + Web.getRequest().setAttribute(FANCY_ERROR_COLLECTION, + listener.getErrors()); + template = new XSLTemplate(source, listener); + } else { + template = new XSLTemplate(source); + } + + } else if (KernelConfig.getConfig().isDebugEnabled() + && template.isModified()) { + // XXX referencing Kernel above is a broken dependency. + // Debug mode should be captured at a lower level, + // probably on UtilConfig. + + if (s_log.isInfoEnabled()) { + s_log.info("Template " + template + " has been modified; " + + "recreating it from scratch"); + } + + if (fancyErrors) { + LoggingErrorListener listener = new LoggingErrorListener(); + Web.getRequest().setAttribute(FANCY_ERROR_COLLECTION, + listener.getErrors()); + template = new XSLTemplate(source, listener); + } else { + template = new XSLTemplate(source); + } + } + + return template; + } + + /** + * Resolves and retrieves the template for the given request. + * + * @param sreq The current request object + * @return The resolved XSLTemplate instance + */ + public static XSLTemplate getTemplate(final HttpServletRequest sreq) { + return getTemplate(sreq, false, true); + } + + /** + * Resolves the template for the given request to an URL. + * + * @param sreq The current request object + * @param fancyErrors Should this place any xsl errors in the request + * for use by another class. If this is true, the + * the errors are stored for later use. + * @param useCache Should the templates be pulled from cache, if available? + * True means they are pulled from cache. False means + * they are pulled from the disk. If this is false + * the pages are also not placed in the cache. + * @return The resolved XSLTemplate instance + */ + public static XSLTemplate getTemplate(final HttpServletRequest sreq, + boolean fancyErrors, + boolean useCache) { + + Assert.exists(sreq, HttpServletRequest.class); + + final URL sheet = getConfig().getStylesheetResolver().resolve(sreq); + Assert.exists(sheet, URL.class); + + return Templating.getTemplate(sheet, fancyErrors, useCache); + } + + /** + * Removes an XSL template from the internal cache. The template + * for source will be regenerated on the next request + * for it. + * + * @param source the URL to the top-level template + * resource + */ + public static synchronized void purgeTemplate(final URL source) { + if (s_log.isDebugEnabled()) { + s_log.debug("Purging cached template for URL " + source); + } + + Assert.exists(source, URL.class); + } + + /** + * Removes all cached template objects. All template objects will + * be regenerated on-demand as each gets requested. + */ + public static synchronized void purgeTemplates() { + if (s_log.isDebugEnabled()) { + s_log.debug("Purging all cached templates"); + } + } + + /** + * Generates a stream containing imports for a number of URLs. + * + * @param paths An iterator of java.net.URL objects + * @return a virtual XSL file + */ + public static InputStream multiplexXSLFiles(Iterator paths) { + // StringBuilder buf = new StringBuilder(); + Element root = new Element("xsl:stylesheet", + "http://www.w3.org/1999/XSL/Transform"); + root.addAttribute("version", "1.0"); + + while (paths.hasNext()) { + URL path = (URL) paths.next(); + + Element imp = root.newChildElement( + "xsl:import", + "http://www.w3.org/1999/XSL/Transform"); + imp.addAttribute("href", path.toString()); + + if (s_log.isInfoEnabled()) { + s_log.info("Adding import for " + path.toString()); + } + } + + Document doc = null; + try { + doc = new Document(root); + } catch (ParserConfigurationException ex) { + throw new UncheckedWrapperException("cannot build document", ex); + } + + if (s_log.isDebugEnabled()) { + s_log.debug("XSL is " + doc.toString(true)); + } + + return new ByteArrayInputStream(doc.toString(true).getBytes()); + } + + /** + * Transforms an URL, that refers to a local resource inside the running + * CCM web application. NON-local URLs remain unmodified. + * + * In case of a virtual path "/resource" it is short-circuiting access to + * the resource servlet. All other http:// URLs are transformed into file:// + * for XSLT validation to work for these resources. It has the added benefit + * of speeding up loading of XSL... + * + * Currently the direct file access method is perferred! As soon as we + * refactor to directly access the published XSL files in the database and + * to avoid the unnecessary intermediate step via the file system, we have + * to use URL's instead to be able to redirect access to a specific address + * to a servlet which manages database retrieval. + */ + static URL transformURL(URL url) { + HttpHost self = Web.getConfig().getHost(); + + /** Indicates whether url refers to a local resource inside the + * running CCM web application (inside it's webapp context) */ + Boolean isLocal = false; + /** Contains the transformed "localized" path to url, i.e. without + * host part. */ + String localPath = ""; + + // Check if the url refers to our own host + if (self.getName().equals(url.getHost()) + && ( (self.getPort() == url.getPort()) + || (url.getPort()== -1 && self.getPort()== 80) + ) + ) { + // host part denotes to a local resource, cut off host part. + localPath = url.getPath(); + isLocal = true; + } + // java.net.URL is unaware of JavaEE webapplication. If CCM is not + // installed into the ROOT context, the path includes the webapp part + // which must be removed as well. + // It's a kind of HACK, unfortunately. If CCM is installed into the + // root context we don't detect whether the first part of the path + // refers to another web application inside the host and assume local + // and unrestricted access. The complete code should get refactored to + // use ServletContext#getResource(path) + String installContext = Web.getWebappContextPath(); + if (s_log.isDebugEnabled()) { + s_log.debug("Installation context is >" + installContext + "<."); + } + if (!installContext.equals("")) { + // CCM is installed into a non-ROOT context + if (localPath.startsWith(installContext)) { + // remove webapp context part + localPath = localPath.substring(installContext.length()); + if (s_log.isDebugEnabled()) { + s_log.debug("WebApp context removed: >>" + + localPath + "<<"); + } + } + } + + if (isLocal) { + // url specifies the running CCM host and port (or port isn't + // specified in url and default for running CCM host + + if (localPath.startsWith("/resource")) { + // A virtual path to the ResourceServlet + localPath = localPath.substring("/resource".length()); //remove virtual part + URL newURL = Web.findResource(localPath); //without host part here! + if (s_log.isDebugEnabled()) { + s_log.debug("Transforming resource " + url + " to " + newURL); + } + return newURL; + } else { + // A real path to disk + final String filename = Web.getServletContext() + .getRealPath(localPath); + File file = new File(filename); + if (file.exists()) { + try { + URL newURL = file.toURL(); + if (s_log.isDebugEnabled()) { + s_log.debug("Transforming resource " + url + " to " + + newURL); + } + return newURL; + } catch (MalformedURLException ex) { + throw new UncheckedWrapperException(ex); + } + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("File " + filename + + " doesn't exist on disk"); + } + } + } + } else { + // url is not the (local) running CCM host, no transformation + // is done + if (s_log.isDebugEnabled()) { + s_log.debug("URL " + url + " is not local"); + } + } + + return url; // returns the original, unmodified url here + } +} + +/** + * + * @author pb + */ +class LoggingErrorListener implements ErrorListener { + + private static final Logger s_log = + Logger.getLogger(LoggingErrorListener.class); + private final ArrayList m_errors; + + LoggingErrorListener() { + m_errors = new ArrayList(); + } + + public Collection getErrors() { + return m_errors; + } + + @Override + public void warning(TransformerException e) throws TransformerException { + log(Level.WARN, e); + } + + @Override + public void error(TransformerException e) throws TransformerException { + log(Level.ERROR, e); + } + + @Override + public void fatalError(TransformerException e) throws TransformerException { + log(Level.FATAL, e); + } + + private void log(Level level, TransformerException ex) { + s_log.log(level, "Transformer " + level + ": " + + ex.getLocationAsString() + ": " + ex.getMessage(), + ex); + m_errors.add(ex); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/TemplatingConfig.java b/ccm-core/src/main/java/com/arsdigita/templating/TemplatingConfig.java new file mode 100755 index 000000000..34f6ec702 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/TemplatingConfig.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import com.arsdigita.runtime.AbstractConfig; +import com.arsdigita.util.parameter.Parameter; +import com.arsdigita.util.parameter.IntegerParameter; +import com.arsdigita.util.parameter.SingletonParameter; +import com.arsdigita.util.parameter.StringParameter; +import org.apache.log4j.Logger; + +/** + * @author Justin Ross + * @version $Id$ + */ +public final class TemplatingConfig extends AbstractConfig { + + /** Internal logger instance to faciliate debugging. Enable logging output + * by editing /WEB-INF/conf/log4j.properties int hte runtime environment + * and set com.arsdigita.templating.TemplatingConfig=DEBUG by uncommenting + * it */ + private static final Logger s_log = Logger.getLogger + (TemplatingConfig.class); + + /** Singelton config object. */ + private static TemplatingConfig s_conf; + /** + * Gain a WorkspaceConfig object. + * + * Singelton pattern, don't instantiate a config object using the + * constructor directly! + * @return + */ + public static synchronized TemplatingConfig getInstanceOf() { + if (s_conf == null) { + s_conf = new TemplatingConfig(); + s_conf.load(); + } + + return s_conf; + } + + /** Fully qualified path string to file contain the pattern file for + {@link com.arsdigita.templating.PatternStylesheetResolver + PatternStylesheetResolver} */ + private final Parameter m_paths = new StringParameter + ("waf.templating.stylesheet_paths", Parameter.REQUIRED, + "/WEB-INF/resources/stylesheet-paths.txt"); + + /** Specifies class for the implementation of StylesheetResolver Interface + to resolve a modules stylesheet. */ + private final Parameter m_resolver = new SingletonParameter + ("waf.templating.stylesheet_resolver", Parameter.REQUIRED, + new PatternStylesheetResolver()); + + /** Specifies number of stylesheets cached. */ + private final Parameter m_cacheSize = new IntegerParameter + ("waf.templating.stylesheet_cache_size", Parameter.OPTIONAL, + null); + + /** Duration of stylesheet cache in seconds */ + private final Parameter m_cacheAge = new IntegerParameter + ("waf.templating.stylesheet_cache_age", Parameter.OPTIONAL, + null); + + public TemplatingConfig() { + + register(m_paths); + register(m_resolver); + register(m_cacheSize); + register(m_cacheAge); + + loadInfo(); + } + + /** + * Get name and location of stylesheet pattern file. + * + * @return String with fully qualified file name + */ + final String getStylesheetPaths() { + return (String) get(m_paths); + } + + /** + * Gets the stylesheet resolver. This value is set via the + * com.arsdigita.templating.stylesheet_resolver + * system property. + * @return + */ + public final StylesheetResolver getStylesheetResolver() { + return (StylesheetResolver) get(m_resolver); + } + + /** Can be null. + * @return */ + public final Integer getCacheSize() { + return (Integer) get(m_cacheSize); + } + + /** Can be null. + * @return */ + public final Integer getCacheAge() { + return (Integer) get(m_cacheAge); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/URLPatternGenerator.java b/ccm-core/src/main/java/com/arsdigita/templating/URLPatternGenerator.java new file mode 100755 index 000000000..a9af92173 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/URLPatternGenerator.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + + +import com.arsdigita.util.Assert; +import com.arsdigita.util.StringUtils; + +import com.arsdigita.web.Web; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.log4j.Logger; +import org.libreccm.web.Application; + + +/** + * Generates a set of pattern values based on the URL path info for the current + * request. Slashes in the request are translated into hyphens; the file + * extension is stripped; any 'index' is removed, except for the top level. + * + * So some examples: + * + * /content/admin/item.jsp -> { "admin-item", "admin", "index" } + * /content/admin/index.jsp -> { "admin", "index" } + * /content/admin/ -> { "admin", "index" } + * /content/index.jsp -> { "index" } + * /content/ -> { "index" } + */ +public class URLPatternGenerator implements PatternGenerator { + + private static final Logger s_log = + Logger.getLogger(URLPatternGenerator.class); + + private static final String DEFAULT_URL_MATCH = "index"; + + /** + * + * @param key + * @param req + * @return + */ + public String[] generateValues(String key, + HttpServletRequest req) { + String path = getPath(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Substituting values for url " + path); + } + + // Check for a file extension & strip it. + int dotIndex = path.lastIndexOf("."); + int slashIndex = path.lastIndexOf("/"); + if (dotIndex > -1 + && dotIndex > slashIndex) { + path = path.substring(0, dotIndex); + } + + // Strip '/index' if any + if (path != null && + path.endsWith("/" + DEFAULT_URL_MATCH)) { + path = path.substring(0, path.length() - + DEFAULT_URL_MATCH.length()); + } + + // Now strip trailing & leading slash + if (path != null && + path.startsWith("/")) { + path = path.substring(1); + } + if (path != null && + path.endsWith("/")) { + path = path.substring(0, path.length()-1); + } + + if (path == null) { + path = ""; + } + + if (s_log.isDebugEnabled()) { + s_log.debug("Normalized path is '" + path + "'"); + } + String[] bits = StringUtils.split(path, '/'); + if (s_log.isDebugEnabled()) { + for (int i = 0 ; i < bits.length ; i++) { + s_log.debug(" -> '" + bits[i] + "'"); + } + } + + // Now we've cut off the file extension, it's time to do the + // funky concatenation trick. + for (int i = 1; i < bits.length; i++) { + bits[i] = bits[i-1] + "-" + bits[i]; + } + + // Now we have to reverse it, so matching goes from most specific + // to most general & add in the default 'index' match + + String[] reverseBits = new String[bits.length+1]; + + for ( int i = bits.length - 1, j = 0; i > -1; i--,j++ ) { + reverseBits[j] = bits [i]; + } + reverseBits[reverseBits.length-1] = DEFAULT_URL_MATCH; + + if (s_log.isDebugEnabled()) { + s_log.debug("After concatenation & reversing"); + for (int i = 0 ; i < reverseBits.length ; i++) { + s_log.debug(" -> '" + reverseBits[i] + "'"); + } + } + + return reverseBits; + } + + + private String getPath() { + String base = getBasePath(); + String url = Web.getWebContext().getRequestURL().getPathInfo(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Base is " + base + " url is " + url); + } + + Assert.isTrue(url.startsWith(base), "URL " + url + " starts with " + base); + + return url.substring(base.length()-1); + } + + /** + * Provides the base URL of the application in the current Web request + * (i.e. application's PrimaryURL). If no application can be found or + no PrimaryURL can be determined ROOT ("/") is returned. + + XXX fix me, why can't we get this from Web.getWebContext().getRequestURL + * + * @return primary url of an application or ROOT + */ + private String getBasePath() { + + // retrieve the application of the request + Application app = Web.getWebContext().getApplication(); + if (app == null) { + return "/"; + } else { + return app.getPrimaryUrl().toString(); + } + + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/WebAppPatternGenerator.java b/ccm-core/src/main/java/com/arsdigita/templating/WebAppPatternGenerator.java new file mode 100755 index 000000000..0e12ae5f5 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/WebAppPatternGenerator.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import com.arsdigita.web.Web; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.log4j.Logger; +import org.libreccm.web.Application; + +/** + * Generates a set of patterns corresponding to the current web application + * prefix. + */ +public class WebAppPatternGenerator implements PatternGenerator { + + /** + * Internal logger instance to faciliate debugging. Enable logging output by + * editing /WEB-INF/conf/log4j.properties int hte runtime environment and + * set com.arsdigita.templating.WebAppPatternGenerator=DEBUG by uncommenting + * or adding the line. + */ + private static final Logger s_log = Logger.getLogger( + WebAppPatternGenerator.class); + + /** + * + * @param key placeholder from the pattern string, without surrounding + * colons, constantly "webapp" here. + * @param req current HttpServletRequest + * + * @return List of webapps contextPath names in an Array of Strings. + */ + @Override + public String[] generateValues(String key, + HttpServletRequest req) { + + Application app = Web.getWebContext().getApplication(); + String ctx = (app == null) ? null : ""; + + if (app == null || ctx == null || "".equals(ctx)) { + ctx = Web.getWebappContextPath(); + } + + // JavaEE requires a leading "/" for web context part, but the pattern + // string already contains a "/", so we have to remove it here to + // too avoid a "//" + if (ctx.startsWith("/")) { + ctx = ctx.substring(1); + } + + if (s_log.isDebugEnabled()) { + s_log.debug("Generating Values key: " + key + " [" + + "Web.getWebContext(): " + Web.getWebContext() + " ," + + "Application: " + Web.getWebContext().getApplication() + + "," + "ContextPath: >" + ctx + "<]"); + } + + /* "Older version: prior 6.6. Some modules used to be installed into + * its own web application context, but needed access to the main + * applications package files (e.g. bebop). Therefore the webapp context + * (ServletContext in API speech) of the main CCM application had to be + * added (which was ROOT by default) + * + * As of version 6.6 all packages are installed in one web application + * context, therefore no additional entry is required. + * This variation had first be introduced with the APLAWS integration + * package, which used to register an additional WebAppPatternGenerator, + * which simply cuts ","+ Web.ROOT_WEBAPP, under a different key + * "Webapp" (singular) */ + // return new String[] { ctx + "," + Web.getRootWebappContextPath() }; + return new String[]{ctx}; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/WrappedTransformerException.java b/ccm-core/src/main/java/com/arsdigita/templating/WrappedTransformerException.java new file mode 100755 index 000000000..33711e5c4 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/WrappedTransformerException.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import com.arsdigita.util.UncheckedWrapperException; + +import javax.xml.transform.TransformerException; + +/** + * A simple wrapper around TransformerException that provides a + * more useful {@link #getMessage()} method. + * + * @author Vadim Nasardinov + * @since 2003-11-21 + * @version $Id$ + */ +public final class WrappedTransformerException extends UncheckedWrapperException { + private TransformerException m_trex; + + /** + * The passed in TransformerException is retrievable later via + * {@link #getRootCause()}. + * + * @pre ex != null + **/ + public WrappedTransformerException(TransformerException ex) { + super(ex); + if ( ex == null ) { throw new NullPointerException("ex"); } + m_trex = ex; + } + + /** + * @see #WrappedTransformerException(TransformerException) + * @pre ex != null + **/ + public WrappedTransformerException(String msg, TransformerException ex) { + super(msg, ex); + if ( ex == null ) { throw new NullPointerException("ex"); } + m_trex = ex; + } + + /** + * The returned message includes the location information. + **/ + public String getMessage() { + StringBuffer sb = new StringBuffer(); + possiblyAppend(sb, super.getMessage()); + appendMessage(m_trex, sb); + return sb.toString(); + } + + private static void appendMessage(TransformerException ex, + StringBuffer sb) { + + if ( !possiblyAppend(sb, ex.getMessageAndLocation()) ) { + possiblyAppend(sb, ex.getMessage()); + possiblyAppend(sb, ex.getLocationAsString()); + } + if ( ex.getCause() instanceof TransformerException ) { + appendMessage((TransformerException) ex.getCause(), sb); + } else { + if ( ex.getCause() != null ) { + possiblyAppend(sb, ex.getCause().getMessage()); + } + } + } + + private static boolean possiblyAppend(StringBuffer sb, String str) { + if ( str == null ) { return false; } + sb.append(str).append("; "); + return true; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/XSLParameterGenerator.java b/ccm-core/src/main/java/com/arsdigita/templating/XSLParameterGenerator.java new file mode 100755 index 000000000..6d6a45600 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/XSLParameterGenerator.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import javax.servlet.http.HttpServletRequest; + +/** + * This is an interface that allows developers to set variables + * that will be available inside of the XSL for use by all XSL stylesheets. + * Implementations of this class can be registered with the + * {@link com.arsdigita.templating.Templating} + * class for use by any {@link com.arsdigita.templating.PresentationManager} + * class + */ +public interface XSLParameterGenerator { + + /** + * This returns the correct value for the parameter. This is the + * value that is added to the transformer and is available to all + * stylesheets + * @param request + * @return + */ + public String generateValue(HttpServletRequest request); +} diff --git a/ccm-core/src/main/java/com/arsdigita/templating/XSLTemplate.java b/ccm-core/src/main/java/com/arsdigita/templating/XSLTemplate.java new file mode 100755 index 000000000..0bc7561f6 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/templating/XSLTemplate.java @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.templating; + +import com.arsdigita.util.Assert; +import com.arsdigita.util.IO; + +import java.io.File; +import java.io.PrintWriter; +import java.io.OutputStream; +import java.io.IOException; + +import java.net.URL; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.xml.transform.ErrorListener; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Templates; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +import org.w3c.dom.Document; + +/** + * A class for loading, caching and generally managing XSL templates and + * transformers. + * + * @author Dan Berrange + * @version $Id$ + */ +public final class XSLTemplate { + + /** Internal logger instance to faciliate debugging. Enable logging output + * by editing /WEB-INF/conf/log4j.properties int hte runtime environment + * and set com.arsdigita.templating.XSLTemplate=DEBUG by uncommenting + * or adding the line. */ + private static final Logger s_log = Logger.getLogger(XSLTemplate.class); + + /** Property containing the URL to the XSL source file or create this + * instance */ + private final URL m_source; + private final Templates m_templates; + private final List m_dependents; + private final Date m_created; + + /** + * Creates and loads a new template from source, + * using listener to handle any errors. + * + * @param source A URL pointing to the template + * source text + * @param listener A ErrorListener to customize + * behavior on error + */ + public XSLTemplate(final URL source, + final ErrorListener listener) { + if (Assert.isEnabled()) { + Assert.exists(source, URL.class); + Assert.exists(listener, ErrorListener.class); + } + + m_source = source; + + final SimpleURIResolver resolver = new SimpleURIResolver(); + + try { + s_log.debug("Getting new templates object"); + + final TransformerFactory factory = + TransformerFactory.newInstance(); + factory.setURIResolver(resolver); + factory.setErrorListener(listener); + + m_templates = factory.newTemplates(resolver.resolve(m_source. + toString(), null)); + + s_log.debug("Done getting new templates"); + } catch (TransformerConfigurationException ex) { + throw new WrappedTransformerException(ex); + } catch (TransformerException ex) { + throw new WrappedTransformerException(ex); + } + + // List contains each include/import URL found in the style sheet + // recursively(!) (i.e. scanning each style sheet whose URL has been + // found in a style sheet, etc. + // In case of Mandalay (single entry stylesheet) about 250 URL's, all + // resolved when found Mandalay's start.xml in one go. + m_dependents = resolver.getStylesheetURIs(); + m_created = new Date(); + } + + /** + * Creates and loads a new template from source using + * the default ErrorListener. + * + * @param source A URL pointing to the template + * source text + */ + public XSLTemplate(final URL source) { + this(source, new Log4JErrorListener()); + } + + /** + * Gets the URL of the template source. + * + * @return The URL location of the template source; + * it cannot be null + */ + public final URL getSource() { + return m_source; + } + + /** + * Gets a list of all dependent stylesheet files. + * + * @return A List of URLs to dependent + * stylesheet files; it cannot be null + */ + public final List getDependents() { + return m_dependents; + } + + /** + * Generates a new Transformer from the internal + * Templates object. + * + * @return The new Transformer; it cannot be null + */ + public final synchronized Transformer newTransformer() { + s_log.debug("Generating new transformer"); + + try { + return m_templates.newTransformer(); + } catch (TransformerConfigurationException tce) { + throw new WrappedTransformerException(tce); + } + } + + /** + * Transforms the source document and sends it to + * result. If there are errors, + * listener handles them. This method internally + * creates and uses a new Transformer. + * + * @param source The Source to be transformed; it + * cannot be null + * @param result The Result to capture the + * transformed product; it cannot be null + * @param listener A ErrorListener to handle + * transformation errors; it cannot be null + */ + public final void transform(final Source source, + final Result result, + final ErrorListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Transforming " + source + " and sending it to " + + result + " using error listener " + listener); + } + + if (Assert.isEnabled()) { + Assert.exists(source, Source.class); + Assert.exists(result, Result.class); + Assert.exists(listener, ErrorListener.class); + } + + try { + final Transformer transformer = newTransformer(); + transformer.setErrorListener(listener); + + s_log.debug("Transforming the XML source document"); + + transformer.transform(source, result); + + s_log.debug("Finished transforming"); + } catch (TransformerConfigurationException tce) { + throw new WrappedTransformerException(tce); + } catch (TransformerException te) { + throw new WrappedTransformerException(te); + } + } + + /** + * Transforms the source document and sends it to + * result. This method internally creates and uses a + * new Transformer. + * + * @param source The Source to be transformed; it + * cannot be null + * @param result The Result to capture the + * transformed product; it cannot be null + */ + public final void transform(final Source source, + final Result result) { + transform(source, result, new Log4JErrorListener()); + } + + /** + * Transforms doc and streams the result to + * writer. If there are errors, + * listener handles them. + * + * @param doc The Document to transform; it cannot be + * null + * @param writer The PrintWriter to receive the + * transformed result; it cannot be null + * @param listener A ErrorListener to handle any + * errors; it cannot be null + */ + public final void transform(final Document doc, + final PrintWriter writer, + final ErrorListener listener) { + if (Assert.isEnabled()) { + Assert.exists(doc, Document.class); + Assert.exists(writer, PrintWriter.class); + Assert.exists(listener, ErrorListener.class); + } + + final DOMSource source = new DOMSource(doc); + final StreamResult result = new StreamResult(writer); + + transform(source, result, listener); + } + + /** + * Transforms doc and streams the result to + * writer. + * + * @param doc The Document to transform; it cannot be + * null + * @param writer The PrintWriter to receive the + * transformed result; it cannot be null + */ + public final void transform(final Document doc, + final PrintWriter writer) { + transform(doc, writer, new Log4JErrorListener()); + } + + /** + * Checks whether the XSL files associated with the template have + * been modified. + * + * @return true if any dependent files have been + * modified, otherwise false + */ + public final boolean isModified() { + if (s_log.isDebugEnabled()) { + s_log.debug("Checking if the XSL files for " + this.getSource().toString() + " " + + "have been modified and need to be re-read"); + } + + final Iterator iter = m_dependents.iterator(); + + while (iter.hasNext()) { + final URL url = Templating.transformURL((URL) iter.next()); + Assert.exists(url, URL.class); + + if (url.getProtocol().equals("file")) { + final File file = new File(url.getPath()); + + if (file.lastModified() > m_created.getTime()) { + if (s_log.isInfoEnabled()) { + s_log.info("File " + file + " was modified " + file. + lastModified()); + } + + return true; + } + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("The URL is not to a file; assuming " + url + + " is not modified"); + } + } + } + + s_log.debug("No files were modified"); + + return false; + } + + /** + * Creates a ZIP file containing this stylesheet and + * all dependant's. NB, this method assumes that all + * stylesheets live in the same URL protocol. If the + * protocol a file is different from the protocol + * of the top level, then this file will be excluded + * from the ZIP. In practice this limitation is not + * critical, because XSL files should always use + * relative imports, which implies all imported files + * will be in the same URL space. + * + * @param os the output stream to write the ZIP to + * @param base the base directory in which the files will extract + * @throws java.io.IOException + */ + public void toZIP(OutputStream os, + String base) + throws IOException { + + final ZipOutputStream zos = new ZipOutputStream(os); + + URL src = getSource(); + String srcProto = src.getProtocol(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Outputting files for " + src); + } + + final Iterator sheets = getDependents().iterator(); + while (sheets.hasNext()) { + URL xsl = (URL) sheets.next(); + if (xsl.getProtocol().equals(srcProto)) { + if (s_log.isDebugEnabled()) { + s_log.debug("Outputting file " + xsl); + } + String path = xsl.getPath(); + if (path.startsWith("/")) { + path = path.substring(1); + } + + zos.putNextEntry(new ZipEntry(base + "/" + path)); + + IO.copy(xsl.openStream(), zos); + } else { + s_log.warn("Not outputting file " + xsl + + " because its not under protocol " + srcProto); + } + } + zos.finish(); + } + + private static class Log4JErrorListener implements ErrorListener { + + @Override + public void warning(TransformerException e) throws TransformerException { + log(Level.WARN, e); + } + + @Override + public void error(TransformerException e) throws TransformerException { + log(Level.ERROR, e); + } + + @Override + public void fatalError(TransformerException e) throws + TransformerException { + log(Level.FATAL, e); + } + + private static void log(Level level, TransformerException ex) { + s_log.log(level, "Transformer " + level + ": " + ex. + getLocationAsString() + ": " + ex.getMessage(), + ex); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/SimplePage.java b/ccm-core/src/main/java/com/arsdigita/ui/SimplePage.java new file mode 100755 index 000000000..11b89b78b --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/SimplePage.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui; + +import com.arsdigita.bebop.BasePage; +import com.arsdigita.bebop.SimpleComponent; +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.Label; +import com.arsdigita.util.Classes; + +import org.apache.log4j.Logger; + +import java.util.HashMap; +import java.util.Iterator; + +/** + * SimplePage is a subclass of ApplicationPage providing a + * simple page which can have custom, application independant + * widgets added to it. The default styling for this + * component provides a border layout with panels for the + * top, bottom, left & right margins of the page. + * + *

Configuration

+ * The components to be added to a page are configured in the + * enterprise.init file using two parameters. The first, + * defaultLayout, specifies the site wide components, + * the second, applicationLayouts allows individual + * applications to be given a custom set of components. + *

+ * The defaultLayout parameter is a list of components + * & their associated layout tags. The values specified for the + * layout tags are handled opaquely by the Java code, passing them + * straight through to the output XML. Thus the exact values you + * can specify are determined by the XSLT used for styling. For + * the default styling rules, the allowable tags are 'top', 'bottom', + * 'left' & 'right'. + *

Generated XML

+ * To allow XSLT to easily distinguish the generic components for the + * page borders from the application specific content, all components + * added to the page are placed within one of two trivial containers. + *

+ * All the application specific components (as added by the add + * method) are placed within a single ui:simplePageContent + * tag. The components for each position tag are placed within a + * ui:simplePagePanel tag, with the position + * attribute set accordingly. + * + */ +public class SimplePage extends BasePage { + private static SimplePageLayout s_default = new SimplePageLayout(); + private static HashMap s_layouts = new HashMap(); + + private static Logger s_log = Logger.getLogger(SimplePage.class); + + /** + * Set the default layout for all applications, which haven't + * got a specific layout configuration. + * + * @param layout the default layout policy + */ + static void setDefaultLayout(SimplePageLayout layout) { + s_default = layout; + } + + /** + * Set the application specific layout, overriding the default + * layout. + * + * @param application the application name + * @param layout the layout policy + */ + static void setLayout(String application, + SimplePageLayout layout) { + s_layouts.put(application, layout); + } + + /** + * Retrieve the layout policy for a particular application. + * Looks for an application specific layout first, and if + * that fails, opts for the default layout. + * + * @param application the application name + * @return the applications layout + */ + static SimplePageLayout getLayout(String application) { + SimplePageLayout layout = (SimplePageLayout)s_layouts.get(application); + + if (layout == null) { + layout = s_default; + } + + return layout; + } + + /** + * Creates a new SimplePage object. This constructor + * is only intended for subclasses & PageFactory. Applications should + * call PageFactory.buildPage to obtain a suitable + * instance of the com.arsdigita.bebop.Page class. + * + * @param application the application name + * @param title label for the page title + * @param id (optionally null) unique id for the page + */ + public SimplePage(String application, + Label title, + String id) { + super(application, title, id); + + setClassAttr("simplePage"); + + addLayoutComponents(application); + } + + /** + * Adds a component to the body of the page. To add a component + * to header / footer / etc, set its classname in one of the + * page layouts. + * @param child the component to add to the body + */ + public void add(Component child) { + if (s_log.isDebugEnabled()) { + s_log.debug("Adding component to body " + child.getClass()); + } + + super.add(child); + } + + /** + * Adds a component to the body of the page. To add a component + * to header / footer / etc, set its classname in one of the + * page layouts. + * @param child the component to add to the body + */ + public void add(Component child, + int constraints) { + if (s_log.isDebugEnabled()) { + s_log.debug("Adding component to body " + child.getClass()); + } + + super.add(child, constraints); + } + + + /** + * Configure this page object, adding the pre-configured + * components to its body + */ + private void addLayoutComponents(String application) { + SimplePageLayout layout = getLayout(application); + + Iterator tags = layout.getPositionTags(); + while (tags.hasNext()) { + String tag = (String)tags.next(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Adding component with tag " + tag); + } + + + Iterator i = layout.getComponents(tag); + while (i.hasNext()) { + Class klass = (Class)i.next(); + SimpleComponent child = (SimpleComponent)Classes.newInstance(klass); + child.setMetaDataAttribute("tag", tag); + + if (s_log.isDebugEnabled()) { + s_log.debug("Adding component " + child.getClass()); + } + + super.add(child); + } + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/SimplePageLayout.java b/ccm-core/src/main/java/com/arsdigita/ui/SimplePageLayout.java new file mode 100755 index 000000000..6627c58d8 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/SimplePageLayout.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Iterator; +import java.util.HashMap; + +import com.arsdigita.util.Assert; +import com.arsdigita.bebop.SimpleComponent; + + +/** + * The SimplePageLayout class stores the list of + * components on a page. Each component has an + * associated layout tag which is interpreted by + * the XSL when formatting the page. If the XSLT + * template in question were implementing a four + * area border layout (header, footer, left & right + * margins), this tag might be one of the strings + * 'top', 'bottom', 'left', 'right'. The Java code + * places no constraints or intpretation of these + * layout tags. + */ +class SimplePageLayout { + + private HashMap m_tags = new HashMap(); + + /** + * Adds a component to the layout with a particular + * position tag. + * + * @param component A subclass of Component with a no-arg constructor + * @param tag the layout position tag + */ + public void addComponent(Class component, + String tag) { + Assert.exists(component, Class.class); + Assert.exists(tag, String.class); + + Assert.isTrue(SimpleComponent.class.isAssignableFrom(component), + "component is a subclass of SimpleComponent"); + + List list = (List)m_tags.get(tag); + if (list == null) { + list = new ArrayList(); + m_tags.put(tag, list); + } + + list.add(component); + } + + /** + * Retrieves an iterator for all the known position tags + * in this layout. + * + * @return an iterator of position tags + */ + public Iterator getPositionTags() { + return m_tags.keySet().iterator(); + } + + /** + * Retrieves an iterator for all components with + * a specified tag. + */ + public Iterator getComponents(String tag) { + List list = (List)m_tags.get(tag); + Assert.exists(list, List.class); + return list.iterator(); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/IO.java b/ccm-core/src/main/java/com/arsdigita/util/IO.java new file mode 100755 index 000000000..910c0197d --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/IO.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +/** + * A library of methods for dealing with I/O streams. + */ +public class IO { + + /** + * Copies the contents of an input stream to another + * output stream. + * + * @param src the source file to be sent + * @param dst the destination to send the file to + */ + public static void copy(InputStream src, + OutputStream dst) + throws IOException { + + byte buf[] = new byte[4096]; + int ret; + + while ((ret = src.read(buf)) != -1) { + dst.write(buf, 0, ret); + } + + dst.flush(); + } + + + // XXX add a method that, given a InputStream + // figures out what character set the containing + // document is, ie reads the XML prolog. + // Also have similar char set discovery APIs + // for HttpServletRequest objects. +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/MessageType.java b/ccm-core/src/main/java/com/arsdigita/util/MessageType.java new file mode 100755 index 000000000..82db306cf --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/MessageType.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +/** + * Special MIME types useful for typing Message objects. + * + * @author Ron Henderson + * @version $Id: MessageType.java 287 2005-02-22 00:29:02Z sskracic $ + */ + +public interface MessageType { + + /** + * MIME type of "text/html" + */ + public final static String TEXT_HTML = "text/html"; + + /** + * MIME type of "text/plain" + */ + public final static String TEXT_PLAIN = "text/plain"; + + /** + * MIME type of "text/plain" with a special format qualifier that + * text should displayed as formatted. + */ + public final static String TEXT_PREFORMATTED = + TEXT_PLAIN + "; format=preformatted"; + + /** + * MIME type of "text/plain" with a special format qualifier that + * simple inline markup should be recognised + */ + public final static String TEXT_SMART = + TEXT_PLAIN + "; format=smart"; +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/OrderedMap.java b/ccm-core/src/main/java/com/arsdigita/util/OrderedMap.java new file mode 100644 index 000000000..621115a42 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/OrderedMap.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.util.TreeMap; +import java.util.HashMap; +import java.util.Comparator; +import org.apache.log4j.Logger; + +/** + * An implementation of Map which preserves the order in which you put + * entries into it. + * + * @deprecated use {@link com.arsdigita.util.SequentialMap} instead + * @author Justin Ross <jross@redhat.com> + * @version $Id$ + */ +public class OrderedMap extends TreeMap { + + private static final Logger s_log = Logger.getLogger(OrderedMap.class); + + private OrderingComparator m_comparator; + + public OrderedMap() { + super(new OrderingComparator()); + + m_comparator = (OrderingComparator) comparator(); + } + + /** + * Calls to put define the order in which the OrderedMap returns + * its contents in calls to entrySet().iterator(); + */ + public Object put(final Object key, final Object value) { + if (s_log.isDebugEnabled()) { + s_log.debug("Adding a new map entry: " + key + " => " + value); + } + + m_comparator.keep(key); + + return super.put(key, value); + } + + public Object clone() { + final OrderedMap result = (OrderedMap) super.clone(); + + result.m_comparator = (OrderingComparator) m_comparator.clone(); + + return result; + } + + public void clear() { + super.clear(); + + m_comparator.clear(); + } +} + +final class OrderingComparator implements Comparator, Cloneable { + private static final Logger s_log = Logger.getLogger + (OrderingComparator.class); + + private HashMap m_sortKeyMap = new HashMap(); + private long m_currSortKey = 0; + + public final int compare(final Object o1, final Object o2) { + Long sk1 = (Long) m_sortKeyMap.get(o1); + Long sk2 = (Long) m_sortKeyMap.get(o2); + + if (sk1 == null) { + if (s_log.isDebugEnabled()) { + s_log.debug("The sort key of " + o1 + " is null; " + + "returning 1"); + } + + return 1; + } else if (sk2 == null) { + if (s_log.isDebugEnabled()) { + s_log.debug("The sort key of " + o2 + " is null; " + + "returning -1"); + } + + return -1; + } else { + final int result = (int) (sk1.longValue() - sk2.longValue()); + + if (s_log.isDebugEnabled()) { + s_log.debug("The sort key of " + o1 + " is " + + sk1.longValue()); + s_log.debug("The sort key of " + o2 + " is " + + sk2.longValue()); + s_log.debug("The result is " + result); + } + + if (Assert.isEnabled() && result == 0) { + Assert.isTrue(o1.equals(o2)); + } + + return result; + } + } + + final void keep(final Object key) { + m_sortKeyMap.put(key, new Long(m_currSortKey++)); + } + + protected Object clone() { + try { + final OrderingComparator result = + (OrderingComparator) super.clone(); + + result.m_sortKeyMap = (HashMap) m_sortKeyMap.clone(); + + return result; + } catch (CloneNotSupportedException cnse) { + // I don't think we can get here. + + return null; + } + } + + final void clear() { + m_sortKeyMap.clear(); + m_currSortKey = 0; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/Pair.java b/ccm-core/src/main/java/com/arsdigita/util/Pair.java new file mode 100755 index 000000000..f96c94395 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/Pair.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.util.Map; + +/** + *

+ * The simplest possible implementation of Map.Entry. Instances + * of this class contains references to the key and + * value set in the constructor. + *

+ * + * @author David Lutterkort + * @version $Id: Pair.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class Pair implements Map.Entry, Cloneable { + + private Object m_key; + private Object m_value; + + /** + *

+ * Creates a new Pair instance. + *

+ * + * @param key the key for this pair. + * @param value the value for this pair. + */ + public Pair(Object key, Object value) { + m_key = key; + m_value = value; + } + + /** + *

+ * Return a shallow copy of this pair. The key and value of the new pair + * refer to the same objects as the key and value in the pair being + * cloned. + *

+ * + * @return Object A new pair, referring to the same key and value + */ + protected Object clone() { + return new Pair(m_key, m_value); + } + + /** + *

+ * Returns the key corresponding to this pair. + *

+ * + * @return Object The key for this pair. + */ + public final Object getKey() { + return m_key; + } + + /** + *

+ * Returns the value corresponding to this pair. + *

+ * + * @return Object The value for this pair. + */ + public final Object getValue() { + return m_value; + } + + /** + *

+ * Replaces the value corresponding to this pair with the specified + * value. + *

+ * + * @param new value to be stored in this entry. + * + * @return Object Old value corresponding to the entry. + */ + public Object setValue(Object value) { + Object oldValue = m_value; + + m_value = value; + + return oldValue; + } + + /** + *

+ * Compare the specified object with this pair. Returns true if the given + * object is also a Map.Entry and its key and value are + * equal to those of this pair. More formally, two entries e1 and e2 + * represent the same mapping if + *

+     * (e1.getKey()==null ?
+     *     e2.getKey()==null : e1.getKey().equals(e2.getKey()))  &&
+     * (e1.getValue()==null ?
+     *     e2.getValue()==null : e1.getValue().equals(e2.getValue()))
+     * 
+ *

+ * + * @param o object to be compared for equality with this pair. + * + * @return boolean true if the specified object is equal to this pair as a + * map entry. + */ + public boolean equals(Object o) { + boolean rv = false; + + if (!(o instanceof Map.Entry)) { + rv = false; + } else { + Map.Entry e = (Map.Entry) o; + rv = ( + (m_key == null ? e.getKey() == null : m_key.equals(e.getKey())) && + (m_value == null ? e.getValue() == null : m_value.equals(e.getValue())) + ); + } + + return rv; + } + + /** + *

+ * The hash code for this pair. The hash code is the bitwise exclusive or + * of the hash codes of the key and the value. If either of these entries + * is null, its hash code is taken to be 0 in + * the exclusive or. + *

+ * + * @return int The hash code of this pair. + */ + public int hashCode() { + return (m_key == null ? 0 : m_key.hashCode()) ^ + (m_value == null ? 0 : m_value.hashCode()); + } + + /** + *

+ * Convert this pair to a String. The returned string is of + * the form key=value where key and + * value are the entries in this pair, converted to + * String. + *

+ * + * @return String of the form key=value + */ + public String toString() { + return m_key + "=" + m_value; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/ParameterProvider.java b/ccm-core/src/main/java/com/arsdigita/util/ParameterProvider.java new file mode 100644 index 000000000..90a7cb205 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/ParameterProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.util.Set; +import javax.servlet.http.HttpServletRequest; + +/** + * + * @version$Id$ + */ +public interface ParameterProvider { + + /** + * Return the set of bebop ParameterModels that this provides. + **/ + public Set getModels(); + /** + * Return the URL parameters for this request as a set of bebop + * ParameterData. + **/ + public Set getParams(HttpServletRequest req); +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/Record.java b/ccm-core/src/main/java/com/arsdigita/util/Record.java new file mode 100644 index 000000000..761dda636 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/Record.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; +import org.apache.log4j.Logger; + +/** + * @author Justin Ross <jross@redhat.com> + * @version $Id$ + */ +public abstract class Record { + + /** Internal logger instance to faciliate debugging. Enable logging output + * by editing /WEB-INF/conf/log4j.properties int the runtime environment + * and set com.arsdigita.util.Record=DEBUG + * by uncommenting or adding the line. */ + private static final Logger s_log = Logger.getLogger(Record.class); + + private Class m_class; + private Logger m_log; + private String[] m_fields; + private boolean m_undergoingAccess = false; + + protected Record(Class clacc, Logger log, String[] fields) { + m_class = clacc; + m_fields = fields; + m_log = log; + } + + protected final void accessed(String field) { + if (m_log.isDebugEnabled()) { + synchronized (this) { + if (m_undergoingAccess == false) { + final Method accessor = accessor(field); + + m_undergoingAccess = true; + final String value = prettyLiteral(value(accessor)); + m_undergoingAccess = false; + + m_log.debug("Returning " + value + " for " + field); + } + } + } + } + + protected final void mutated(String field) { + if (m_log.isInfoEnabled()) { + final Method accessor = accessor(field); + + m_undergoingAccess = true; + final String value = prettyLiteral(value(accessor)); + m_undergoingAccess = false; + + m_log.info(field + " set to " + value); + } + } + + private String prettyLiteral(final Object o) { + if (o == null) { + return null; + } else if (o instanceof String) { + return "\"" + o + "\""; + } else { + return o.toString(); + } + } + + private Method accessor(final String field) { + try { + Method method = m_class.getDeclaredMethod + ("get" + field, new Class[] {}); + + return method; + } catch (NoSuchMethodException nsme) { + try { + Method method = m_class.getDeclaredMethod + ("is" + field, new Class[] {}); + + return method; + } catch (NoSuchMethodException me) { + throw new UncheckedWrapperException(nsme); + } + } + } + + private Object value(final Method m) { + try { + return m.invoke(this, new Object[] {}); + } catch (IllegalAccessException iae) { + throw new UncheckedWrapperException(iae); + } catch (InvocationTargetException ite) { + throw new UncheckedWrapperException(ite); + } + } + + public final String getCurrentState() { + final StringBuffer info = new StringBuffer(); + + for (int i = 0; i < m_fields.length; i++) { + final Method method = accessor(m_fields[i]); + final String name = method.getName(); + final String value = prettyLiteral(value(method)); + final int len = name.length(); + + if (len < 30) { + for (int j = 0; j < 30 - len; j++) { + info.append(' '); + } + } + + info.append(name); + info.append("() -> "); + info.append(value); + info.append("\n"); + } + + return info.toString(); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/SequentialMap.java b/ccm-core/src/main/java/com/arsdigita/util/SequentialMap.java new file mode 100755 index 000000000..c23b3b388 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/SequentialMap.java @@ -0,0 +1,373 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + *

+ * A map that keeps its entries in a fixed sequence. All iterators returned + * by this class, for example by entrySet().iterator(), are + * guaranteed to return the entries in the order in which they were put in + * the map. This implementation allows null for both the key + * or the associated value for a map entry. + *

+ * + *

+ * Almost all of the map operations, for example {@link #get get} or {@link + * #containsKey containsKey} require time linear in the size of the map, + * making this map only suitable for small map sizes. + *

+ * + * @author David Lutterkort + * @version $Id: SequentialMap.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class SequentialMap extends AbstractMap implements Map { + + private ArrayList m_entries = null; + private Set m_entrySet = null; + + /** + *

+ * Creates an empty SequentialMap. + *

+ */ + public SequentialMap() { + m_entries = new ArrayList(); + } + + /** + *

+ * Return the number of entries in the map. + *

+ * + * @return int the number of entries. + */ + public int size() { + return m_entries.size(); + } + + /** + *

+ * Index of the entry with the given key. key may be null. + *

+ * + *

+ * Requires time linear in the size of the map. + *

+ * + * @param key the key to find. + * + * @return int the index with key key or -1 if no such entry + * exists. + */ + public int indexOf(Object key) { + int index = -1; + + for (int i = 0; i < size(); i++) { + Map.Entry p = (Map.Entry) m_entries.get(i); + + if (key == null) { + if (p.getKey() == null) { + index = i; + } + } else { + if (key.equals(p.getKey())) { + index = i; + } + } + } + + return index; + } + + /** + *

+ * Return true if the map maps one or more keys to the specified + * value. More formally, returns true if and only if this map contains at + * least one mapping to a value v such that + * (value==null ? v==null : value.equals(v)). + *

+ * + *

+ * Requires time linear in the size of the map + *

+ * + * @param value value whose presence in this map is to be tested. + * + * @return true if this map maps one or more keys to the + * specified value. + */ + public boolean containsValue(Object value) { + boolean contains = false; + + for (int i = 0; i < size(); i++) { + Map.Entry p = (Map.Entry) m_entries.get(i); + + if (value == null) { + if (p.getValue() == null) { + contains = true; + } + } else { + if (value.equals(p.getValue())) { + contains = true; + } + } + } + + return contains; + } + + /** + *

+ * Returns true if this map contains a mapping for the + * specified key. + *

+ * + *

+ * Requires time linear in the size of the map. + *

+ * + * @param key key whose presence in this map is to be tested. + * + * @return boolean true if this map contains a mapping for the + * specified key. + */ + public boolean containsKey(Object key) { + return (indexOf(key) != -1); + } + + /** + *

+ * Returns the value to which this map maps the specified key. Returns + * null if the map contains no mapping for this key. A + * return value of null does not necessarily indicate that + * the map contains no mapping for the key; it's also possible that the + * map explicitly maps the key to null. The + * containsKey operation may be used to distinguish these + * two cases. + *

+ * + *

+ * Requires time linear in the size of the map. + *

+ * + * @param key key whose associated value is to be returned. + * + * @return Object the value to which this map maps the specified key, or + * null if the map contains no mapping for this key. + */ + public Object get(Object key) { + int i = indexOf(key); + + return (i == -1) ? null : ((Map.Entry) m_entries.get(i)).getValue(); + } + + /** + *

+ * Returns the value which is stored at the specified sequential + * position in the map. May throw an IndexOutOfBoundsException + * if (index < 0 || index >= size()). + *

+ * + *

+ * Requires constant time. + *

+ * + * @param index The index of the element to return. + * + * @return Object The element at the specified index. + */ + public Object get(int index) { + return ((Map.Entry) m_entries.get(index)).getValue(); + } + + /** + *

+ * Returns the key which is stored at the specified sequential + * position in the map. May throw an IndexOutOfBoundsException + * if (index < 0 || index >= size()). + *

+ * + *

+ * Requires constant time. + *

+ * + * @param index The index of the element to return. + * + * @return Object The key of the element at the specified index. + */ + public Object getKey(int index) { + return ((Map.Entry) m_entries.get(index)).getKey(); + } + + /** + * Associates the specified value with the specified key in this map. The + * new entry is appended at the end of the map. If an entry with the same + * key already exists, it is removed first. To change the + * value of an existing key-value pair without changing the position of + * the key, use {@link #update update}. + * + *

+ * Requires time linear in the size of the map + * + * @param key key with which the specified value is to be associated. + * + * @param value value to be associated with the specified key. + * + * @return Object The previous value associated with specified key, or + * null if there was no mapping for key. A null return can also + * indicate that the map previously associated null with the + * specified key. + * + * @see #update update + */ + public Object put(Object key, Object value) { + Object result = null; + int i = indexOf(key); + + if (i != -1) { + result = ((Pair) m_entries.get(i)).getValue(); + m_entries.remove(i); + } + m_entries.add(new Pair(key, value)); + + return result; + } + + /** + *

+ * Update an existing key-value pair. If an entry with key + * key already exists, it is replaced with the new + * association without changing the place in which the key appears in the + * sequence of keys. If no such entry exists, it is appended at the end. + *

+ * + *

+ * Requires time linear in the size of the map. + *

+ * + * @param key key with which the specified value is to be associated. + * + * @param value value to be associated with the specified key. + * + * @return Object The previous value associated with specified key, or + * null if there was no mapping for key. A null return can also + * indicate that the map previously associated null with the + * specified key. + * + * @see #put put + */ + public Object update(Object key, Object value) { + Object result = null; + int i = indexOf(key); + + if (i != -1) { + result = ((Pair) m_entries.get(i)).getValue(); + m_entries.set(i, new Pair(key, value)); + } else { + m_entries.add(new Pair(key, value)); + } + + return result; + } + + /** + *

+ * Removes the mapping for this key from this map if present. + *

+ * + * @param key key whose mapping is to be removed from the map. + * + * @return Object The previous value associated with specified key, or null + * if there was no mapping for key. A null return can also indicate + * that the map previously associated null with the specified key. + */ + public Object remove(Object key) { + Object result = null; + int i = indexOf(key); + + if (i != -1) { + result = ((Pair) m_entries.get(i)).getValue(); + m_entries.remove(i); + } + + return result; + } + + /** + *

+ * Removes all mappings from this map. + *

+ */ + public void clear() { + m_entries.clear(); + } + + /** + *

+ * Returns a set view of the mappings contained in this map. Each element + * in the returned set is a Map.Entry. The set is backed by + * the map, so changes to the map are reflected in the set, and + * vice-versa. If the map is modified while an iteration over the set is + * in progress, the results of the iteration are undefined. The set + * supports element removal, which removes the corresponding mapping from + * the map, via the Iterator.remove, + * Set.remove, removeAll, + * retainAll and clear operations. It does not + * support the add or addAll operations. + *

+ * + * @return Set A set view of the mappings contained in this map. + * + * @post return != null + */ + public Set entrySet() { + if (m_entrySet == null) { + m_entrySet = new AbstractSet() { + public Iterator iterator() { + return m_entries.iterator(); + } + + public boolean contains(Object o) { + return m_entries.contains(o); + } + + public boolean remove(Object o) { + return m_entries.remove(o); + } + + public int size() { + return m_entries.size(); + } + + public void clear() { + m_entries.clear(); + } + }; + } + + return m_entrySet; + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/SystemInformation.java b/ccm-core/src/main/java/com/arsdigita/util/SystemInformation.java new file mode 100644 index 000000000..d926e2ca9 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/SystemInformation.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2014 Jens Pelzetter + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; + +/** + * Provides the system name of the CCM Spin off (eg aplaws or ScientificCMS) and the version number. + * It's primary use is to provide the theme engine with that information for display. The data + * displayed is stored in the /WEB-INF/systeminformation.properties, which is usually provided by + * the bundle. The ccm-sci-bundle for example provides this file, which can be found in + * {@code ccm-sci-bundle/web/WEB-INF} directory. At the moment it is necessary to update this + * (these) file(s) manually. + * + * A {@code systeminformations.properties} should contain at least these three properties: + *
+ *
version
+ *
The version of the specific CCM distribution.
+ *
appname
+ *
The name of the CCM distribution, for example ScientificCMS + *
apphomepage
+ *
+ The URL of the website of the CCM distribution, for example + * {@code http://www.scientificcms.org} + *
+ *
+ * + * + * @author Jens Pelzetter + * @version $Id$ + */ +public class SystemInformation { + + /** + * Map containing all informations provided by the {@code systeminformation.properties} file. + */ + private final Map sysInfo = new HashMap(); + /** + * The one and only instance of this class + */ + private final static SystemInformation INSTANCE = new SystemInformation(); + + /** + * The constructor takes care of loading the data from the properties file and placing them into + * {@code HashMap}. + */ + public SystemInformation() { + + final Properties properties = new Properties(); + try { + properties.load(getClass().getResourceAsStream( + "/WEB-INF/systeminformation.properties")); + } catch (IOException ex) { + throw new UncheckedWrapperException(ex); + } + for (String key : properties.stringPropertyNames()) { + sysInfo.put(key, properties.getProperty(key)); + } + + } + + /** + * @return The instance of this class. + */ + public static SystemInformation getInstance() { + return INSTANCE; + } + + /* + * Get system informations by key. + * + * @param key Key for the map + * + * @return value for key + * + * @throws IllegalArgumentException if key is null or empty + */ + final public String get(final String key) throws IllegalArgumentException { + if (key == null || key.isEmpty()) { + throw new IllegalArgumentException("Parameter key must not be null or empty."); + } + return sysInfo.get(key); + } + + /** + * Get iterator of this map. + * + * @return iterator of map + */ + final public Iterator> iterator() { + return sysInfo.entrySet().iterator(); + } + + /** + * + * @return + */ + final public boolean isEmpty() { + return sysInfo.isEmpty(); + + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/URLRewriter.java b/ccm-core/src/main/java/com/arsdigita/util/URLRewriter.java new file mode 100644 index 000000000..b541f9367 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/URLRewriter.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.net.URLEncoder; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpUtils; +import org.apache.log4j.Logger; + +/** + * Re-writes URLs to include additional parameters that come from a + * set of registered ParameterProviders. This makes + * cookieless login possible, by re-writing URLs to include a session + * ID parameter. + * + * @version $Id$ + */ +public class URLRewriter { + + /** Creates a s_logging category with name = to the full name of class */ + private static final Logger s_log = Logger.getLogger(URLRewriter.class); + + /** The parameter providers for the system. Client classes are registered here. */ + private static LinkedList s_providers = new LinkedList(); + + /** + * Adds a parameter provider. + **/ + public static void addParameterProvider(ParameterProvider provider) { + s_log.debug("addParameterProvider: " + + provider.getClass().getName()); + s_providers.add(provider); + } + + /** + * Clears all parameter providers. + **/ + public static void clearParameterProviders() { + s_providers = new LinkedList(); + } + /** + * Returns the set of global parameter models, or the empty set if no + * provider is set. + * + * @return a set of bebop ParameterModels + **/ + public static Set getGlobalModels() { + if (s_providers.isEmpty()) { + s_log.debug("getGlobalModels: no providers set"); + return java.util.Collections.EMPTY_SET; + } + + Set rs = new HashSet(); + for (Iterator i = s_providers.iterator(); i.hasNext();) { + rs.addAll(((ParameterProvider) i.next()).getModels()); + } + return rs; + } + + /** + * Returns the set of global URL parameters for the given request, or + * the empty set if no provider is set. + * + * @return a set of bebop ParameterData + **/ + public static Set getGlobalParams(HttpServletRequest req) { + if (s_providers.isEmpty()) { + s_log.debug("getGlobalParams: no providers set"); + return java.util.Collections.EMPTY_SET; + } + + Set rs = new HashSet(); + for (Iterator i = s_providers.iterator(); i.hasNext();) { + rs.addAll(((ParameterProvider)i.next()).getParams(req)); + } + return rs; + } + + /** + * Encodes the given URL for redirecting the client. Adds ACS global + * parameters and servlet session parameters to the URL. The + * sendRedirect(req, resp, url) method calls this method automatically. + * + * @return the new URL + **/ + public static String encodeRedirectURL(HttpServletRequest req, + HttpServletResponse resp, + String url) { + if (s_log.isDebugEnabled()) { + s_log.debug("encodeRedirectURL: before: " + url); + } + + url = resp.encodeRedirectURL(encodeParams(req, url)); + + if (s_log.isDebugEnabled()) { + s_log.debug("encodeRedirectURL: after: " + url); + } + + return url; + } + + /** + * Prepares the given URL for the client. No effect if no provider is + * set. + * + * @return the prepared URL + * + * @deprecated This method does not encode the servlet session ID. Use + * encodeURL(req, res, url) instead. + **/ + public static String prepareURL(String url, HttpServletRequest req) { + return encodeParams(req, url); + } + + /** + * Encodes the given URL for the client. Adds ACS global parameters and + * servlet session parameters to the URL. If the URL will be used for + * redirection, use sendRedirect(req, resp, url) instead. + * + * @return the new URL + **/ + public static String encodeURL(HttpServletRequest req, + HttpServletResponse resp, + String url) { + if (s_log.isDebugEnabled()) { + s_log.debug("encodeURL: before: " + url); + } + + url = resp.encodeURL(encodeParams(req, url)); + + if (s_log.isDebugEnabled()) { + s_log.debug("encodeURL: after: " + url); + } + + return url; + } + + /** + * Adds the ACS global params to the URL. + **/ + private static String encodeParams(HttpServletRequest req, String url) { + if (s_providers.isEmpty()) { + s_log.debug("encodeParams: no providers set"); + return url; + } + Map params = new java.util.HashMap(); + String base = parseQueryString(url, params); + merge(getGlobalParams(req), params); + url = base + unparseQueryString(params); + return url; + } + + /** + * Merges a set of bebop ParameterData into a URL parameter map. + **/ + private static void merge(Set data, Map params) { + Iterator values = data.iterator(); + while (values.hasNext()) { + Map.Entry value = (Map.Entry)values.next(); + if (value == null) { + continue; + } + params.put(value.getKey(), value.getValue()); + } + } + + /** + * Parses the given URL into a non-query part and a URL parameter map. + * + * @param url the original URL + * + * @param params map from param name to value. Each value is a String + * if parameter is single-valued; String[] if multi-valued. Existing + * values will be blindly overwritten by this method. + * + * @return the non-query part of the URL + **/ + private static String parseQueryString(String url, Map params) { + int qmark = url.indexOf('?'); + if (qmark < 0) { + return url; + } + String base = url.substring(0, qmark); + String query = url.substring(qmark+1); + params.putAll(HttpUtils.parseQueryString(query)); + return base; + } + + /** + * Returns the query string representation of the given URL parameter + * map, including leading question mark. Should be appended to the + * return value of a previous call to parseQueryString(). Handles + * multi-valued parameters correctly. Ignores null values. + **/ + private static String unparseQueryString(Map params) { + StringBuffer buf = new StringBuffer(128); + char sep = '?'; + Iterator keys = params.keySet().iterator(); + while (keys.hasNext()) { + String key = (String)keys.next(); + Object value = params.get(key); + if (value instanceof String[]) { + String[] values = (String[])value; + for (int i = 0; i < values.length; i++) { + if (values[i] != null) { + appendParam(buf, sep, key, values[i]); + sep = '&'; + } + } + continue; + } else if (value != null) { + appendParam(buf, sep, key, value.toString()); + sep = '&'; + } + } + return buf.toString(); + } + + /** + * Appends string representation of a parameter to the given + * StringBuffer: sep + URLEncode(key) + '=' + URLEncode(value) + **/ + private static void appendParam(StringBuffer buf, char sep, + String key, String value) { + buf.append(sep).append(URLEncoder.encode(key)) + .append('=').append(URLEncoder.encode(value)); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/servlet/HttpHost.java b/ccm-core/src/main/java/com/arsdigita/util/servlet/HttpHost.java new file mode 100644 index 000000000..74396c544 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/servlet/HttpHost.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util.servlet; + +import com.arsdigita.util.Assert; +import javax.servlet.http.HttpServletRequest; +import org.apache.log4j.Logger; + +/** + * Represents a host computer. The host may in fact be a "virtual" + * host, one of several on the same physical machine. + * + * @author Dan Berrange + * @author Justin Ross <jross@redhat.com> + * @version $Id$ + */ +public class HttpHost { + + private static final Logger s_log = Logger.getLogger(HttpHost.class); + + private final String m_name; + private final int m_port; + + /** + * Constructs a new host named name and on port + * port. + * + * @param name A String host name, for example + * "ccm.redhat.com"; see {@link + * javax.servlet.ServletRequest#getServerName()}; it cannot be + * null + * @param port An int port number; 8080, + * for instance; see {@link + * javax.servlet.ServletRequest#getServerPort()}; it must be + * greater than 0 + */ + public HttpHost(final String name, final int port) { + if (Assert.isEnabled()) { + Assert.exists(name, String.class); + Assert.isTrue(port > 0, + "The port must be greater than 0; " + + "I got " + port); + } + + m_name = name; + m_port = port; + } + + /** + * Constructs a host representing the host-specific part of + * sreq. + * + * @param sreq An HttpServletRequest representation + * of a request; it cannot be null + */ + public HttpHost(final HttpServletRequest sreq) { + final String header = sreq.getHeader("Host"); + + if (header == null) { + if (s_log.isInfoEnabled()) { + s_log.info("No 'Host:' header present; falling back " + + "on values from servlet request"); + } + + m_name = sreq.getServerName(); // XXX use httpserver + m_port = sreq.getServerPort(); + } else { + final int colon = header.indexOf( ':' ); + + if (colon == -1) { + m_name = header; + + // Internet Explorer doesn't include the port number + // in the Host: header, so if your server *appears* to + // be on port 80, we take a look at the actual server + // port to verify. + // + // NB. So for vHosting to work, you must make sure + // your web server is using the same port as your + // squid proxy + final String agent = sreq.getHeader("User-Agent"); + + if (agent != null + && agent.toLowerCase().indexOf("msie") >= 0) { + m_port = sreq.getServerPort(); // XXX use httpserver + } else { + m_port = 80; + } + } else { + m_name = header.substring(0, colon); + m_port = Integer.parseInt + (header.substring(colon + 1, header.length())); + } + } + } + + /** + * Gets the host name. + * + * @return A String naming the host; it cannot be + * null + */ + public final String getName() { + return m_name; + } + + /** + * Gets the port of this host. + * + * @return A int port number + */ + public final int getPort() { + return m_port; + } + + final void toString(final StringBuffer buffer) { + buffer.append(getName()); + + final int port = getPort(); + + if (port != 80) { + buffer.append(":"); + buffer.append(port); + } + } + + /** + * Returns a String representation of this host. + * + * @return getName() + ":" + getPort() or simply + * getName() if the port is 80 + */ + @Override + public String toString() { + final StringBuffer buffer = new StringBuffer(24); + + toString(buffer); + + return buffer.toString(); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/servlet/HttpHostParameter.java b/ccm-core/src/main/java/com/arsdigita/util/servlet/HttpHostParameter.java new file mode 100644 index 000000000..9583b9c56 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/servlet/HttpHostParameter.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util.servlet; + +import com.arsdigita.util.parameter.ErrorList; +import com.arsdigita.util.parameter.ParameterError; +import com.arsdigita.util.parameter.StringParameter; +import org.apache.log4j.Logger; + +/** + * This class represents info about a single host running + * a server in a webapp cluster. + */ +public class HttpHostParameter extends StringParameter { + private static final Logger s_log = Logger.getLogger + (HttpHostParameter.class); + + public HttpHostParameter(final String name) { + super(name); + } + + public HttpHostParameter(final String name, + final int multiplicity, + final Object defaalt) { + super(name, multiplicity, defaalt); + } + + protected Object unmarshal(final String value, final ErrorList errors) { + if (value.indexOf("://") != -1) { + final ParameterError error = new ParameterError + (this, "The value must not have a scheme prefix"); + errors.add(error); + } + + if (value.indexOf("/") != -1) { + final ParameterError error = new ParameterError + (this, "The value must not contain slashes"); + errors.add(error); + } + + final int sep = value.indexOf(":"); + + if (sep == -1) { + final ParameterError error = new ParameterError + (this, "The value must contain a colon"); + errors.add(error); + } + + if (!errors.isEmpty()) { + return null; + } + + try { + final String name = value.substring(0, sep); + final String port = value.substring(sep + 1); + + return new HttpHost(name, Integer.parseInt(port)); + } catch (IndexOutOfBoundsException ioobe) { + final ParameterError error = new ParameterError + (this, "The host spec is invalid; it must take the form " + + "hostname:hostport"); + errors.add(error); + + return null; + } catch (NumberFormatException nfe) { + final ParameterError error = new ParameterError + (this, "The port number must be an integer with no " + + "extraneous spaces or punctuation"); + errors.add(error); + + return null; + } + } + + protected String marshal(Object value) { + if (value == null) { + return null; + } else { + final HttpHost host = (HttpHost) value; + return host.getName() + ":" + host.getPort(); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/ApplicationFileResolver.java b/ccm-core/src/main/java/com/arsdigita/web/ApplicationFileResolver.java new file mode 100644 index 000000000..8ee1208d8 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/ApplicationFileResolver.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +import org.libreccm.web.Application; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.RequestDispatcher; + + +/** + * Interface specifies standard API tools to resolve an URL to a accessible + * resource, stored in file system, database of any other suitable location. + * The URL may include virtual resources, e.g. files stored in the database + * instead of the file system. The URL may include other "virtual" parts with + * must be mapped to an appropriate real path. + */ +public interface ApplicationFileResolver { + + /** + * + * @param templatePath + * @param sreq + * @param sresp + * @param app + * @return + */ + RequestDispatcher resolve(String templatePath, + HttpServletRequest sreq, + HttpServletResponse sresp, + Application app); + +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/BaseApplicationServlet.java b/ccm-core/src/main/java/com/arsdigita/web/BaseApplicationServlet.java new file mode 100644 index 000000000..c2499b2f0 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/BaseApplicationServlet.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2015 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package com.arsdigita.web; + + +import org.apache.log4j.Logger; +import org.libreccm.cdi.utils.CdiLookupException; +import org.libreccm.cdi.utils.CdiUtil; +import org.libreccm.web.Application; +import org.libreccm.web.ApplicationRepository; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + *

+ * The base servlet for CCM applications. It manages database transactions, + * prepares an execution context for the request, and traps and handles requests + * to redirect.

+ * + *

+ * Most CCM applications will extend this class by implementing + * {@link #doService(HttpServletRequest,HttpServletResponse,Application)} to + * perform application-private dispatch to UI code.

+ * + *

+ * The application will be available at the path + * www.example.org/ccm/applicationname, where + * applicationname is the name defined for the application and + * www.example.org the URL of the server. + *

+ * + * @see com.arsdigita.web.BaseServlet + * @see com.arsdigita.web.DispatcherServlet + * @see com.arsdigita.web.RedirectSignal + * + * @author Justin Ross + * <jross@redhat.com> + * @author + * Jens Pelzetter + */ +public abstract class BaseApplicationServlet extends BaseServlet { + + private static final long serialVersionUID = 3204787384428680311L; + + private static final Logger s_log = Logger.getLogger( + BaseApplicationServlet.class); + + /** + *

+ * The ID of the application whose service is requested. This request + * attribute must be set by a previous servlet or filter before this servlet + * can proceed. In CCM, the default servlet, {@link DispatcherServlet}, sets + * this attribute using the {@link BaseDispatcher}. + * Important: This does only work if the application is + * called using an URL like + * http://www.example.org/ccm/application!

+ */ + public static final String APPLICATION_ID_ATTRIBUTE + = BaseApplicationServlet.class.getName() + + ".application_id"; + + /** + *

+ * The same as {@link #APPLICATION_ID_ATTRIBUTE}, but as a request + * parameter. This is present so applications not using the dispatcher + * servlet may accept requests directly to their servlets, provided the + * application ID is given in the URL.

+ */ + public static final String APPLICATION_ID_PARAMETER = "app-id"; + + /** + *

+ * Augments the context of the request and delegates to {@link + * #doService(HttpServletRequest,HttpServletResponse,Application)}.

+ * + * @throws javax.servlet.ServletException + * @throws java.io.IOException + * @see + * com.arsdigita.web.BaseServlet#doService(HttpServletRequest,HttpServletResponse) + */ + @Override + protected void doService(final HttpServletRequest request, + final HttpServletResponse response) + throws ServletException, IOException { + + final Application app = getApplication(request); + + if (app == null) { + response.sendError(404, "Application not found"); + throw new IllegalStateException("Application not found"); + } + + Web.getWebContext().setApplication(app); + +// final RequestContext rc = makeLegacyContext( +// request, app, Web.getUserContext()); +// +// DispatcherHelper.setRequestContext(request, rc); +// +// final ServletException[] servletException = {null}; +// final IOException[] ioException = {null}; + + doService(request, response, app); + } + + /** + * The method that + * {@link #doService(HttpServletRequest,HttpServletResponse)} calls. Servlet + * authors should implement this method to perform application-specific + * request handling + * + * @see + * javax.servlet.http.HttpServlet#service(HttpServletRequest,HttpServletResponse) + * + * @param sreq + * @param sresp + * @param app + * + * @throws javax.servlet.ServletException + * @throws java.io.IOException + */ + protected abstract void doService(final HttpServletRequest sreq, + final HttpServletResponse sresp, + final Application app) + throws ServletException, IOException; + + /** + * + * @param sreq + * + * @return + */ + private Application getApplication(final HttpServletRequest request) { + s_log.debug("Resolving the application that will handle this request"); + + Long appId = (Long) request.getAttribute(APPLICATION_ID_ATTRIBUTE); + + if (appId == null) { + s_log.debug("I didn't receive an application ID with the " + + "servlet request; trying to get it from the " + + "query string"); + + final String value = request.getParameter(APPLICATION_ID_PARAMETER); + + if (value != null) { + try { + appId = Long.getLong(value); + } catch (NumberFormatException ex) { + throw new IllegalStateException("Could not parse '" + value + + "' into a long"); + } + } + } + + if (s_log.isDebugEnabled()) { + s_log.debug("Retrieving application " + appId + " from the " + + "database"); + } + + final CdiUtil cdiUtil = new CdiUtil(); + try { + final ApplicationRepository appRepo = cdiUtil.findBean( + ApplicationRepository.class); + + return appRepo.findById(appId); + } catch (CdiLookupException ex) { + throw new IllegalStateException(String.format( + "Failed to retrieve application %d from the database.", appId)); + } + } + + /** + * + * @param sreq + * @param app + * @param uc + * @return + */ +// private RequestContext makeLegacyContext(HttpServletRequest sreq, +// final Application app, +// final UserContext uc) { +// s_log.debug("Setting up a legacy context object"); +// +// sreq = DispatcherHelper.restoreOriginalRequest(sreq); +// +// final InitialRequestContext irc = new InitialRequestContext +// (sreq, getServletContext()); +// final SessionContext sc = uc.getSessionContext(); +// +// final KernelRequestContext krc = new KernelRequestContext +// (irc, sc, uc); +// +// return krc; +// } + + +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/BaseServlet.java b/ccm-core/src/main/java/com/arsdigita/web/BaseServlet.java new file mode 100644 index 000000000..0790ca923 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/BaseServlet.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2015 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package com.arsdigita.web; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * + * @author Jens Pelzetter + */ +public abstract class BaseServlet extends HttpServlet { + + private static final long serialVersionUID = -3402624854177649796L; + + private static final Logger LOGGER = LogManager.getFormatterLogger( + BaseServlet.class); + + public static final String REQUEST_URL_ATTRIBUTE = BaseServlet.class + .getName() + ".request_url"; + + /** + * Initializer uses parent class's initializer to setup the servlet request, + * response and application context. Usually a user of this class will not + * overwrite this method but the user extension point doInit to perform + * local initialization tasks! + * + * @param config + * + * @throws javax.servlet.ServletException + */ + @Override + public final void init(final ServletConfig config) throws ServletException { + LOGGER.info("Initialising servlet %s (class: %s)...", + config.getServletName(), + getClass().getName()); + + super.init(config); + + //Check if we not the ResourceManager for CCM NG. Also check if we + //can replace static instance with an application scoped CDI Bean + //ResourceManager.getInstance().setServletContext(getServletContext); + doInit(); + } + + protected void doInit() throws ServletException { + //Empty + } + + @Override + public final void destroy() { + LOGGER.info("Destroying servlet %s...", + getServletConfig().getServletName()); + + doDestroy(); + } + + protected void doDestroy() { + /// Empty + } + + private void internalService(final HttpServletRequest request, + final HttpServletResponse response) + throws ServletException, IOException { + //This method was present in the old implemention and was responsible + //for managing the application managed transactions. Because we now use + //container managed transactions we may don't need this method anymore. + + doService(request, response); + } + + /** + *

+ * The method that {@link + * #doGet(HttpServletRequest,HttpServletResponse)} and {@link + * #doPost(HttpServletRequest,HttpServletResponse)} call. This is the + * extension point for users of this class.

+ * + * @param request + * @param response + * + * @throws javax.servlet.ServletException + * @throws java.io.IOException + */ + protected abstract void doService(final HttpServletRequest request, + final HttpServletResponse response) + throws ServletException, IOException; + + /** + *

+ * Processes HTTP GET requests.

+ * + * @param request + * @param response + * + * @throws javax.servlet.ServletException + * @throws java.io.IOException + * @see + * javax.servlet.http.HttpServlet#doGet(HttpServletRequest,HttpServletResponse) + */ + @Override + protected final void doGet(final HttpServletRequest request, + final HttpServletResponse response) + throws ServletException, IOException { + LOGGER.info("Serving GET request path %s with servlet %s (class: %s)", + request.getPathInfo(), + getServletConfig().getServletName(), + getClass().getName()); + + internalService(request, response); + } + + /** + *

Processes HTTP POST requests.

+ * + * @param request + * @param response + * @throws javax.servlet.ServletException + * @throws java.io.IOException + * + * @see javax.servlet.http.HttpServlet#doPost(HttpServletRequest,HttpServletResponse) + */ + @Override + protected final void doPost(final HttpServletRequest request, + final HttpServletResponse response) + throws ServletException, IOException { + LOGGER.info("Serving POST request path %s with servlet %s (class: %s)", + request.getPathInfo(), + getServletConfig().getServletName(), + getClass().getName()); + + internalService(request, response); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/CCMDispatcherServlet.java b/ccm-core/src/main/java/com/arsdigita/web/CCMDispatcherServlet.java new file mode 100644 index 000000000..00f36acbe --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/CCMDispatcherServlet.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2015 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package com.arsdigita.web; + +import com.arsdigita.dispatcher.DispatcherHelper; +import com.arsdigita.util.Assert; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.libreccm.web.Application; +import org.libreccm.web.ApplicationRepository; +import org.libreccm.web.ServletPath; + +import java.io.IOException; +import java.math.BigDecimal; + +import javax.inject.Inject; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * + * @author Jens Pelzetter + */ +@WebServlet(urlPatterns = {"/ccm/*"}, + loadOnStartup = 1) +public class CCMDispatcherServlet extends BaseServlet { + + private static final long serialVersionUID = 5292817856022435529L; + + private static final Logger LOGGER = LogManager.getFormatterLogger( + CCMDispatcherServlet.class); + + private static final String DISPATCHED_ATTRIBUTE + = CCMDispatcherServlet.class + .getName() + ".dispatched"; + + /** + * String containing the web context path portion of the WEB application + * where this CCMDispatcherServlet is executed. (I.e. where the WEB-INF + * directory containing the web.xml configuring this CCMDispatcherServlet is + * located in the servlet container webapps directory. + * + */ + private static String s_contextPath; + + @Inject + private transient ApplicationRepository appRepository; + + /** + * Servlet initializer uses the extension point of parent class. + * + * @throws ServletException + */ + @Override + public void doInit() throws ServletException { + + ServletContext servletContext = getServletContext(); + s_contextPath = servletContext.getContextPath(); + // For backwords compatibility reasons register the web application + // context of the Core (root) application als "/" + // Web.registerServletContext("/", + // servletContext); + + } + + @Override + protected void doService(final HttpServletRequest request, + final HttpServletResponse response) + throws ServletException, IOException { + + LOGGER.debug("Dispatching request %s [ %s, %s, %s, %s ]", + request.getRequestURI(), + request.getContextPath(), + request.getPathInfo(), + request.getQueryString()); + + final String path = request.getPathInfo(); + + if (requiresTrailingSlash(path)) { + LOGGER.debug("The request URI needs a trailing slash. Redirecting"); + + final String prefix = DispatcherHelper.getDispatcherPrefix(request); + String uri = request.getRequestURI(); + if (prefix != null && prefix.trim().length() > 0) { + uri = prefix + uri; + } + final String query = request.getQueryString(); + + if (query == null) { + response.sendRedirect(response.encodeRedirectURL(uri + "/")); + } else { + response.sendRedirect(response + .encodeRedirectURL(uri + "/?" + query)); + } + } else { + LOGGER.debug("Storing the path elements of the current request as " + + "the original path elements"); + + request.setAttribute(BaseServlet.REQUEST_URL_ATTRIBUTE, + new URL(request)); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Using path '" + path + "' to lookup application"); + } + + final ApplicationSpec spec = lookupApplicationSpec(path); + + if (spec == null) { + LOGGER.debug("No application was found; doing nothing"); + // return false; + // we have to create a 404 page here! + String requestUri = request.getRequestURI(); // same as ctx.getRemainingURLPart() + response.sendError(404, requestUri + " not found on this server."); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Found application " + spec.getAppID() + "; " + + "dispatching to its servlet"); + } + + request.setAttribute + (BaseApplicationServlet.APPLICATION_ID_ATTRIBUTE, + spec.getAppID()); + request.setAttribute(DISPATCHED_ATTRIBUTE, Boolean.TRUE); + forward(spec.getTypeContextPath(), spec.target(path), request, response); + // return true; + } + + } + + } + + private boolean requiresTrailingSlash(final String path) { + LOGGER.debug("Checking if the required needs a trailing slash..."); + + if (path == null) { + LOGGER.debug("The path is null; the request needs a trailing slash"); + return true; + } + + if (path.endsWith("/")) { + LOGGER.debug("The path already ends in '/'"); + return false; + } + + if (path.lastIndexOf(".") < path.lastIndexOf("/")) { + LOGGER.debug("The last fragment of the path has no '.', so we " + + "assume a directory was requested; a trailing " + + "slash is required"); + return true; + } else { + LOGGER.debug("The last fragment of the path appears to be a file " + + "name; no trailing slash is needed"); + return false; + } + } + + private void forward(final String contextPath, + final String target, + final HttpServletRequest request, + final HttpServletResponse response) + throws ServletException, IOException { + + LOGGER.debug("Forwarding by path to target \"%s\"...", target); + LOGGER.debug("The context path is: %s", contextPath); + final String forwardContextPath; + if (contextPath == null || contextPath.isEmpty()) { + //Not compliant with Servlet specification. + //Empty context has be be "/" + forwardContextPath = "/"; + } else if (!contextPath.endsWith("/")) { + //No trailing slash, add one + forwardContextPath = String.format("%s/", contextPath); + } else { + forwardContextPath = contextPath; + } + + final ServletContext context = getServletContext().getContext( + forwardContextPath); + + LOGGER.debug("forwarding from context \"%s\" to context \"%s\"...", + getServletContext(), context); + + forward(context.getRequestDispatcher(target), + request, + response); + } + + private void forward(final RequestDispatcher dispatcher, + final HttpServletRequest request, + final HttpServletResponse response) + throws ServletException, IOException { + LOGGER.debug("Checking if this request need to be forwarded or " + + "included: %s", request); + + if (request.getAttribute("javax.servlet.include.request_uri") == null) { + LOGGER.debug("The attribute javax.servlet.include.request_uri " + + "is not set; forwarding %s", + request); + + dispatcher.forward(request, response); + } else { + LOGGER.debug("The attribute javax.servlet.include.request_uri " + + "is set; including %s", + request); + dispatcher.include(request, response); + } + } + + /** + * + * @param path + * @return + */ + private ApplicationSpec lookupApplicationSpec(final String path) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("*** Starting application lookup for path '" + + path + "' ***"); + } + + final Application application = appRepository + .retrieveApplicationForPath(path); + + if (application == null) { + LOGGER.warn("No application found for path \"%s\"."); + return null; + } else { + return new ApplicationSpec(application); + } + } + + public static String getContextPath() { + return s_contextPath; + } + + /** + * + */ + /* Nothing specifically to destroy here + @Override + protected void doDestroy() { + } + */ + + + + /** + * Private class. + */ + private static class ApplicationSpec { + private final long m_id; + private final String m_instanceURI; + private final String m_typeURI; + private final String m_typeContextPath; + + /** + * + * @param app + */ + ApplicationSpec(Application app) { + if ( app == null ) { throw new NullPointerException("app"); } + + m_id = app.getObjectId(); + m_instanceURI = app.getPrimaryUrl().toString(); + if (app.getClass().isAnnotationPresent(ServletPath.class)) { + m_typeURI = app.getClass().getAnnotation(ServletPath.class).value(); + } else { + m_typeURI = URL.SERVLET_DIR + "/legacy-adapter"; + } + m_typeContextPath = ""; + + if (Assert.isEnabled()) { + Assert.exists(m_id, BigDecimal.class); + Assert.exists(m_instanceURI, String.class); + Assert.exists(m_typeURI, String.class); + Assert.exists(m_typeContextPath, String.class); + } + } + + /** + * + * @return + */ + long getAppID() { return m_id; } + + /** + * Provides the context the application is executing. Usually all CCM + * applications will now execute in the samme webapp context. The + * app.getContextPath() return "" in this case where an application is + * executing in no specific context but CCM's default. + * @return The context path of the application's url, "" in case of + * executing in the ROOT context. + */ + String getTypeContextPath() { + if (m_typeContextPath.equals("") ) { + // app is running in CCM's default context, determine the + // actual one + return Web.getWebappContextPath(); + } else { + return m_typeContextPath; + } + } + + /** + * + * @param path + * @return + */ + String target(final String path) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Building the target path from the request path '" + + path + "' and the spec " + this); + } + + final StringBuffer target = new StringBuffer(128); + + target.append(m_typeURI); + target.append(path.substring(m_instanceURI.length())); + target.append("?"); + target.append(BaseApplicationServlet.APPLICATION_ID_PARAMETER); + target.append("="); + target.append(m_id); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Returning target value '" + target + "'"); + } + + return target.toString(); + } + + /** + * + * @param obj + * @return + */ + @Override + public boolean equals(Object obj) { + if ( obj==null ) { return false; } + + ApplicationSpec other = (ApplicationSpec) obj; + return m_id == other.getAppID() && + equal(m_instanceURI, other.m_instanceURI) && + equal(m_typeURI, other.m_typeURI) && + equal(m_typeContextPath, other.m_typeContextPath); + + } + + /** + * + * @param s1 + * @param s2 + * @return + */ + private boolean equal(String s1, String s2) { + if (s1==s2) { return true; } + if (s1==null) { return equal(s2, s1); } + return s1.equals(s2); + } + + /** + * + * @return + */ + @Override + public int hashCode() { + return toString().hashCode(); + } + + /** + * + * @return + */ + @Override + public String toString() { + final String sep = ", "; + StringBuilder sb = new StringBuilder(); + sb.append("["); + sb.append("appID=").append(m_id).append(sep); + sb.append("instanceURI=").append(m_instanceURI).append(sep); + sb.append("typeURI=").append(m_typeURI).append(sep); + sb.append("typeContextPath=").append(m_typeContextPath); + return sb.append("]").toString(); + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/Debugger.java b/ccm-core/src/main/java/com/arsdigita/web/Debugger.java new file mode 100755 index 000000000..cd0949812 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/Debugger.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +import com.arsdigita.kernel.KernelConfig; + +import java.util.ArrayList; +import java.util.Iterator; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.log4j.Logger; + +/** + * Debugger contains static methods for registering debuggers. + * Typically, debuggers are written to display the contents of + * internal CCM data structures e.g., the XML representation of a page + * prior to transformation. Subclass this class to add a particular + * type of debugger. + * + * @see TransformationDebugger + * + * @author Justin Ross + * @version $Id: Debugger.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public abstract class Debugger { + + private static final Logger s_log = Logger.getLogger(Debugger.class); + + public static final String DEBUG_PARAMETER = "debug"; + public static final ThreadLocal s_debuggers = new DebuggerListLocal(); + + public static class DebugParameterListener implements ParameterListener { + public void run(HttpServletRequest sreq, ParameterMap map) { + if (KernelConfig.getConfig().isDebugEnabled()) { + final String value = sreq.getParameter(DEBUG_PARAMETER); + + if (value != null) { + map.setParameter(DEBUG_PARAMETER, value); + } + } + } + } + + public static final void addDebugger(Debugger debugger) { + ArrayList list = (ArrayList) s_debuggers.get(); + list.add(debugger); + } + + public static final String getDebugging(HttpServletRequest sreq) { + ArrayList list = (ArrayList) s_debuggers.get(); + Iterator iter = list.iterator(); + StringBuffer buffer = new StringBuffer(); + + while (iter.hasNext()) { + Debugger debug = (Debugger) iter.next(); + + if (debug.isRequested(sreq)) { + buffer.append(debug.debug()); + } + } + + return buffer.toString(); + } + + public abstract boolean isRequested(HttpServletRequest sreq); + + public abstract String debug(); + + private static class DebuggerListLocal extends InternalRequestLocal { + @Override + protected Object initialValue() { + if (KernelConfig.getConfig().isDebugEnabled()) { + return new ArrayList(); + } else { + return null; + } + } + + @Override + protected void clearValue() { + if (KernelConfig.getConfig().isDebugEnabled()) { + ArrayList list = (ArrayList) get(); + list.clear(); + } + } + } +} + diff --git a/ccm-core/src/main/java/com/arsdigita/web/DefaultApplicationFileResolver.java b/ccm-core/src/main/java/com/arsdigita/web/DefaultApplicationFileResolver.java new file mode 100644 index 000000000..b19221931 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/DefaultApplicationFileResolver.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.RequestDispatcher; + +import org.apache.log4j.Logger; +import org.libreccm.web.Application; + + +/** + * The default implementation deals with templates files belonging to a specific + * application, e.g. cms. Because of the modular structure of CCM all file + * resources of an application are stored below that application's module + * directory. The directory structure itself is application specific. + */ +public class DefaultApplicationFileResolver implements ApplicationFileResolver { + + /** Internal logger instance to faciliate debugging. Enable logging output + * by editing /WEB-INF/conf/log4j.properties int hte runtime environment + * and set com.arsdigita.web.DefaultApplicationFileResolver=DEBUG by + * uncommenting or adding the line. */ + private static final Logger s_log = Logger.getLogger + (DefaultApplicationFileResolver.class); + + /** List of alternative greeting files. Typical vales are index.jsp and + * index.html */ + private static final String[] WELCOME_FILES = new String[] { + "index.jsp", "index.html" + }; + + /** + * Determines from the passsed in request URL a suitable template file in + * the templates subdirectory. It returns an identified template wrapped + * in a RequestDispatcher enabling it to be executed (forwarded). The + * request will typically something like + *
/[appCtx]/[webappInstance]/[webappInstInternalDir]/[template.jsp]
+ * For the content section "info" administration page installed in the + * ROOT context (i.e. [appCtx] is empty) in would be + *
/info/admin/index.jsp
+ * The actual template is actual stored in the file system at + *
/templates/ccm-cms/content-section/admin/index.jsp
and the + * content-section to be administrated has to be passed in as parameter. + * + * @param templatePath + * @param sreq + * @param sresp + * @param app + * @return + */ + @Override + public RequestDispatcher resolve(String templatePath, + HttpServletRequest sreq, + HttpServletResponse sresp, + Application app) { + + String pathInfo = sreq.getPathInfo(); // effectively provides an url + if (s_log.isDebugEnabled()) { // with application part stripped + s_log.debug("Resolving resource for " + pathInfo); + } + + // determine the URL the application INSTANCE is really installed at + // will replace the application part stripped above + String node = app.getPrimaryUrl().toString(); + + do { + + // First check the complete path for the instance. Parameter + // templatePath denotes the template directory for the application + // TYPE. + String path = templatePath + node + pathInfo; + + // Just in case of a directory the list of welcome files have to be + // probed. + if (path.endsWith("/")) { + for (String welcomeFile : WELCOME_FILES) { //1.5 enhanced for-loop + if (s_log.isDebugEnabled()) { + s_log.debug("Trying welcome resource " + + path + welcomeFile); + } + RequestDispatcher rd = Web.findResourceDispatcher( + "" + path + welcomeFile); + if (rd != null) { + if (s_log.isDebugEnabled()) { + s_log.debug("Got dispatcher " + rd); + } + return rd; + } + } + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("Trying resource " + path); + } + + RequestDispatcher rd = Web.findResourceDispatcher( + "" + path); + if (rd != null) { + if (s_log.isDebugEnabled()) { + s_log.debug("Got dispatcher " + rd); + } + return rd; + } + } + + // If nothing has been found at the complete path, probe variations + // of the node part by clipping element-wise + if ("".equals(node)) { + // if node is already empty we can't clip anything - fallthrough + node = null; + } else { + // clipp the last part of node retaining the first / in case + // of multiple parts or clip at all (in case of a single part) + int index = node.lastIndexOf("/", node.length() - 2); + node = node.substring(0, index); + } + } while (node != null); + + if (s_log.isDebugEnabled()) { + s_log.debug("No dispatcher found"); + } + // fallthrough, no success - returning null + return null; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/DynamicHostProvider.java b/ccm-core/src/main/java/com/arsdigita/web/DynamicHostProvider.java new file mode 100644 index 000000000..10b6c7059 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/DynamicHostProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005 RuntimeCollective Ltd. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +public interface DynamicHostProvider { + + public String getName(); + + public int getPort(); +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/InternalRequestLocal.java b/ccm-core/src/main/java/com/arsdigita/web/InternalRequestLocal.java new file mode 100644 index 000000000..6e4ff6d45 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/InternalRequestLocal.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +import java.util.ArrayList; +import java.util.Iterator; +import javax.servlet.http.HttpServletRequest; +import org.apache.log4j.Logger; + +/** + * A class that provides request-framed control over a thread-local + * value. With such control, it is possible to safely reuse + * thread-local data across requests. For example, the following + * InternalRequestLocal reuses a HashMap. + * + *
+ * class HashMapRequestLocal extends InternalRequestLocal { + * protected Object initialValue() { + * return new HashMap(); + * } + * + * // Does not override prepareValue(HttpServletRequest). + * // InternelRequestLocal's default implementation calls + * // clearValue() to prepare the value for the request. + * + * protected void clearValue() { + * ((HashMap) get()).clear(); + * } + * } + *
+ * + *

initialValue() is called just once, when the value + * is first accessed. prepareValue(HttpServletRequest) + * is called at the start of every request serviced by {@link + * com.arsdigita.web.BaseServlet}. clearValue() is + * called at the end of every request handled by said servlet.

+ * + *

The default implementation of clearValue() sets the + * value to null, and the default implementation of + * prepareValue(HttpServletRequest) calls + * clearValue(). As a result, an + * InternalRequestLocal as used in the following example + * is similar to using a request attribute.

+ * + *
+ * // This value is s_servletContext.set(null) at the start and end of + * // each request. + * static ThreadLocal s_servletContext = new InternalRequestLocal(); + *
+ * + *

Be advised that errors in using this class can easily result in + * excess trash left on threads and, worse, big memory leaks. Please + * use caution.

+ * + * @see java.lang.ThreadLocal + * @see com.arsdigita.web.BaseServlet + * @author Justin Ross <jross@redhat.com> + * @version $Id$ + */ +class InternalRequestLocal extends ThreadLocal { + + private static final Logger s_log = + Logger.getLogger(InternalRequestLocal.class); + + private static final ArrayList s_locals = new ArrayList(); + + /** + *

Constructs a new InternalRequestLocal and registers it to be + * initialized and cleared on each request.

+ */ + public InternalRequestLocal() { + super(); + + s_locals.add(this); + } + + /** + * + * @param sreq + */ + static void prepareAll(final HttpServletRequest sreq) { + if (s_log.isDebugEnabled()) { + s_log.debug("Initializing all request-local objects; there are " + + s_locals.size()); + } + + final Iterator iter = s_locals.iterator(); + + while (iter.hasNext()) { + final InternalRequestLocal local = (InternalRequestLocal) iter.next(); + + local.prepareValue(sreq); + } + } + + /** + * + */ + static void clearAll() { + if (s_log.isDebugEnabled()) { + s_log.debug("Clearing all request-local objects; there are " + + s_locals.size()); + } + + final Iterator iter = s_locals.iterator(); + + while (iter.hasNext()) { + final InternalRequestLocal local = (InternalRequestLocal) iter.next(); + + local.clearValue(); + } + } + + /** + *

Called at the start of each request, this method returns the + * request-initialized value of the thread-local variable.

+ * + *

By default this method calls clearValue().

+ * + * @param sreq the current servlet request + * @return the request-initialized value + */ + protected void prepareValue(HttpServletRequest sreq) { + clearValue(); + } + + /** + *

Called at the end of each request, this method clears the + * thread-local value.

+ * + *

By default this method calls set(null). Users + * of this class may override this method to better reuse the + * thread-local value.

+ */ + protected void clearValue() { + set(null); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/ParameterListener.java b/ccm-core/src/main/java/com/arsdigita/web/ParameterListener.java new file mode 100644 index 000000000..ad85def8b --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/ParameterListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author Justin Ross <jross@redhat.com> + * @version $Id$ + */ +public interface ParameterListener { + + void run(HttpServletRequest sreq, ParameterMap map); + +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/ParameterMap.java b/ccm-core/src/main/java/com/arsdigita/web/ParameterMap.java new file mode 100644 index 000000000..8767ba263 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/ParameterMap.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +import com.arsdigita.util.Assert; +import com.arsdigita.util.OrderedMap; +import com.arsdigita.util.UncheckedWrapperException; +import org.apache.commons.codec.net.URLCodec; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; +import org.apache.log4j.Logger; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * @author Justin Ross <jross@redhat.com> + * @version $Id$ + */ +public class ParameterMap implements Cloneable { + + private static final Logger s_log = Logger.getLogger(ParameterMap.class); + + private static ArrayList s_listeners = new ArrayList(); + + private OrderedMap m_params; + + public ParameterMap() { + m_params = new OrderedMap(); + } + + // Expects an *encoded* query string, just as + // request.getQueryString() returns. + private ParameterMap(final String query) { + this(); + + if (query != null) { + parseQueryString(query); + } + } + + public ParameterMap(final HttpServletRequest sreq) { + this(); + + final Enumeration keys = sreq.getParameterNames(); + + while (keys.hasMoreElements()) { + final String name = (String) keys.nextElement(); + final String[] values = (String[]) sreq.getParameterValues(name); + + setParameterValues(name, values); + } + } + + public ParameterMap(final Map params) { + this(); + + final Set keySet = params == null ? null : params.keySet(); + if (keySet != null) { + final Iterator keys = keySet.iterator(); + while (keys.hasNext()) { + final String name = (String)keys.next(); + final String[] values = (String[])params.get(name); + + setParameterValues(name, values); + } + } + } + + public static final ParameterMap fromString(final String query) { + Assert.exists(query, "String query"); + + if (query.startsWith("?")) { + return new ParameterMap(query.substring(1)); + } else { + return new ParameterMap(query); + } + } + + public static final void registerListener + (final ParameterListener listener) { + if (s_log.isDebugEnabled()) { + s_log.debug("Registering parameter listener " + listener); + } + + s_listeners.add(listener); + } + + public Object clone() throws CloneNotSupportedException { + final ParameterMap result = (ParameterMap) super.clone(); + + result.m_params = (OrderedMap) m_params.clone(); + + return result; + } + + private void parseQueryString(final String query) { + final int len = query.length(); + int start = 0; + + while (true) { + int end = -1; + + for (int i = start; i < len - 1; i++) { + if (query.charAt(i) == '&' || query.charAt(i) == ';') { + end = i; + + break; + } + } + + if (end == -1) { + if (len > start) { + try { + parseParameter(query, start, len); + } catch (DecoderException e) { + throw new UncheckedWrapperException(e); + } + } + + break; + } else { + try { + parseParameter(query, start, end); + } catch (DecoderException e) { + throw new UncheckedWrapperException(e); + } + start = end + 1; + } + } + } + + private void parseParameter(final String query, + final int start, + final int end) throws DecoderException { + final int sep = query.indexOf('=', start); + + if (Assert.isEnabled()) { + Assert.isTrue(start > -1); + Assert.isTrue(end > -1); + } + + if (sep > -1) { + URLCodec codec = new URLCodec(); + final String name = codec.decode(query.substring(start, sep)); + final String value = codec.decode + (query.substring(sep + 1, end)); + + if (s_log.isDebugEnabled()) { + s_log.debug("Parameter " + name + " = " + value); + } + + final String[] values = getParameterValues(name); + + if (values == null) { + setParameter(name, value); + } else { + final String[] newValues = new String[values.length + 1]; + + for (int i = 0; i < values.length; i++) { + newValues[i] = values[i]; + } + + newValues[values.length] = value; + + setParameterValues(name, newValues); + } + } + } + + private void validateName(final String name) { + Assert.exists(name, "String name"); + Assert.isTrue(!name.equals(""), + "The name must not be the empty string"); + Assert.isTrue(name.indexOf(" ") == -1, + "The name must not contain any spaces: '" + + name + "'"); + } + + public final void clear() { + m_params.clear(); + } + + public final String getParameter(final String name) { + final String[] values = (String[]) m_params.get(name); + + if (values == null) { + return null; + } else { + return values[0]; + } + } + + /** + * Sets the parameter name to value. If + * value is null, this method sets the value to the + * empty string. + * + * Use of this method assumes that the parameter has only one + * value; if you wish to give a parameter multiple values, use + * {@link #setParameterValues(String, String[])}. + * + * @param name The String name of the parameter + * @param value The String value of the parameter + * @see javax.servlet.ServletRequest#getParameter(String) + * @pre name != null && !name.trim().equals("") + */ + public final void setParameter(final String name, final String value) { + if (Assert.isEnabled()) { + validateName(name); + } + + if (value == null) { + m_params.put(name, new String[] {""}); + } else { + m_params.put(name, new String[] {value}); + } + } + + /** + * A convenience method that calls {@link #setParameter(String, + * String)} using value.toString(). If + * value is null, it is converted to the empty + * string. + * + * @param name The String name of the parameter + * @param value The Object value of the parameter + * @pre name != null && !name.trim().equals("") + */ + public final void setParameter(final String name, final Object value) { + if (value == null) { + setParameter(name, ""); + } else { + setParameter(name, value.toString()); + } + } + + public final String[] getParameterValues(final String name) { + return (String[]) m_params.get(name); + } + + public final void setParameterValues(final String name, + final String[] values) { + if (Assert.isEnabled()) { + validateName(name); + Assert.exists(values, "String[] values"); + Assert.isTrue(values.length > 0, + "The values array must have at least one value"); + } + + m_params.put(name, values); + } + + public final void clearParameter(final String name) { + if (Assert.isEnabled()) { + validateName(name); + } + + m_params.remove(name); + } + + public final Map getParameterMap() { + if (m_params.isEmpty()) { + return null; + } else { + return Collections.unmodifiableMap(m_params); + } + } + + public final String toString() { + if (m_params.isEmpty()) { + return ""; + } else { + return "?" + makeQueryString(); + } + } + + public final String getQueryString() { + return makeQueryString(); + } + + public final void runListeners(final HttpServletRequest sreq) { + final Iterator iter = s_listeners.iterator(); + + while (iter.hasNext()) { + final ParameterListener listener = (ParameterListener) iter.next(); + + listener.run(sreq, this); + } + } + + final String makeQueryString() { + final StringBuffer buffer = new StringBuffer(); + final Iterator iter = m_params.entrySet().iterator(); + URLCodec codec = new URLCodec(); + + while (iter.hasNext()) { + final Map.Entry entry = (Map.Entry) iter.next(); + final String key = (String) entry.getKey(); + final String[] values = (String[]) entry.getValue(); + + if (Assert.isEnabled()) { + Assert.isTrue(key.indexOf('%') == -1, + "The key '" + key + "' has already been " + + "encoded"); + } + + if (values != null) { + if (Assert.isEnabled()) { + Assert.isTrue(values.toString().indexOf('%') == -1, + "One of the values " + + Arrays.asList(values) + " has " + + "already been encoded"); + } + + for (int i = 0; i < values.length; i++) { + try { + buffer.append(codec.encode(key)); + } catch (EncoderException e) { + throw new UncheckedWrapperException(e); + } + buffer.append('='); + + final String value = values[i]; + + if (value != null) { + try { + buffer.append(codec.encode(value)); + } catch (EncoderException e) { + throw new UncheckedWrapperException(e); + } + } + + buffer.append('&'); + } + } + } + + int last = buffer.length() - 1; + + if (last > -1 && buffer.charAt(last) == '&') { + buffer.deleteCharAt(last); + } + + return buffer.toString(); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/RedirectSignal.java b/ccm-core/src/main/java/com/arsdigita/web/RedirectSignal.java new file mode 100644 index 000000000..71a4020df --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/RedirectSignal.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +import com.arsdigita.util.Assert; +import org.apache.log4j.Logger; + +/** + *

+ * A signal that requests to commit or abort the current transaction and to send + * a redirect to a new URL. BaseServlet traps this signal when it is thrown and + * finishes the transaction before it sends the redirect to the response. This + * way the client cannot see state inconsistent with work performed in the + * previous request.

+ * + *

+ * RedirectSignals are usually sent after doing work on behalf of + * the user:

+ * + *
+ * private final void saveUserSettings(final HttpServletRequest sreq) {
+ *     m_user.setGivenName("Gibbon");
+ *     m_user.setFamilyName("Homily");
+ *
+ *     m_user.save();
+ *
+ *     // The boolean argument true signifies that we want to commit
+ *     // the transaction.
+ *     throw new RedirectSignal(URL.here(sreq, "/user-detail.jsp"), true);
+ * }
+ * 
+ * + * @see com.arsdigita.web.BaseServlet + * @see com.arsdigita.web.LoginSignal + * @see com.arsdigita.web.ReturnSignal + * @author Justin Ross <jross@redhat.com> + * @version $Id$ + */ +public class RedirectSignal extends TransactionSignal { + + private static final long serialVersionUID = 102910405551194091L; + + /** + * Logger instance for debugging support. + */ + private static final Logger s_log = Logger.getLogger(RedirectSignal.class); + + /** + * Destination URL where redirect to + */ + private final String m_url; + + /** + * Constructor + * + * @param url + * @param isCommitRequested + */ + public RedirectSignal(final String url, final boolean isCommitRequested) { + super(isCommitRequested); + + if (Assert.isEnabled()) { + Assert.exists(url, "String url"); + Assert.isTrue(url.startsWith("http") || url.startsWith("/"), + "The URL is relative and won't dispatch " + + "correctly under some servlet containers; " + + "the URL is '" + url + "'"); + } + + if (s_log.isDebugEnabled()) { + s_log.debug("Request for redirect to URL '" + url + "'", + new Throwable()); + } + + m_url = url; + } + + /** + * Convenience Constructor for URL objects. + * + * @param url + * @param isCommitRequested + */ + public RedirectSignal(final URL url, final boolean isCommitRequested) { + this(url.toString(), isCommitRequested); + } + + public final String getDestinationURL() { + return m_url; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/TransactionSignal.java b/ccm-core/src/main/java/com/arsdigita/web/TransactionSignal.java new file mode 100644 index 000000000..5600170b4 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/TransactionSignal.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +import org.apache.log4j.Logger; + +/** + *

+ * A signal to BaseServlet requesting that the current transaction + * be committed or aborted. As with all exceptions, throwing a + * TransactionSignal stops the execution of currently running + * code.

+ * + * @author Justin Ross <jross@redhat.com> + * @version $Id$ + */ +class TransactionSignal extends Error { + + private static final long serialVersionUID = -6081887476661858043L; + + private static final Logger s_log = Logger + .getLogger(TransactionSignal.class); + + + private final boolean m_isCommitRequested; + + TransactionSignal(boolean isCommitRequested) { + if (s_log.isDebugEnabled()) { + s_log.debug("Constructing a transaction signal with " + + "isCommitRequested set " + isCommitRequested); + } + + m_isCommitRequested = isCommitRequested; + + } + + public final boolean isCommitRequested() { + return m_isCommitRequested; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/TransformationDebugger.java b/ccm-core/src/main/java/com/arsdigita/web/TransformationDebugger.java new file mode 100755 index 000000000..0fd1b7a4e --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/TransformationDebugger.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +import com.arsdigita.dispatcher.DispatcherHelper; +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Document; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Iterator; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.log4j.Logger; + +/** + * + * A debugger that displays the original XML source of a document prior to + * transformation (only applies if using Bebop JSP), the generated XML document + * before transformation, and the XSL stylesheet files used for transformation. + * + * To view a page using this debugger, pass "debug=transform" in as a query + * variable. + * + * @see com.arsdigita.bebop.jsp.ShowPage + * + * @author Justin Ross + * <jross@redhat.com> + * @version $Id: TransformationDebugger.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class TransformationDebugger extends Debugger { + + private static final Logger s_log = Logger.getLogger( + TransformationDebugger.class); + + // private Document m_original; + // private Document m_source; + private URL m_sheet; + private List m_dependents; + + /** + * The value passed in to the "debug" query string that activates this + * particular debugger. + */ + public static final String TRANSFORM_DEBUG_VALUE = "transform"; + + // Debuggers are per-request objects. + /** + * @pre sheet != null + * @pre dependents != null + * + */ + public TransformationDebugger(Document original, + Document source, + URL sheet, + List dependents) { + Assert.exists(sheet, URL.class); + Assert.exists(sheet, List.class); + // m_original = original; + // m_source = source; + m_sheet = sheet; + m_dependents = dependents; + } + + /** + * @see #TransformationDebugger(Document, Document, URL, List) + * + */ + public TransformationDebugger(URL sheet, List dependents) { + this(null, null, sheet, dependents); + } + + public boolean isRequested(HttpServletRequest sreq) { + String value = sreq.getParameter(DEBUG_PARAMETER); + + return value != null && value.indexOf(TRANSFORM_DEBUG_VALUE) != -1; + } + + public String debug() { + StringBuffer buffer = new StringBuffer(1024); + + buffer.append("

The Stylesheet files

"); + buffer.append("
    "); + + try { + Iterator sources = m_dependents.iterator(); + + File root = new File(DispatcherHelper.getRequestContext() + .getServletContext().getRealPath("/")); + String base = root.toURL().toExternalForm(); + + while (sources.hasNext()) { + String path = sources.next().toString(); + + if (path.startsWith(base)) { + path = path.substring(base.length()); + } + + buffer.append("
  • " + path + + "
  • "); + } + } catch (IOException ioe) { + throw new Error(ioe); + } + + buffer.append("
"); + return buffer.toString(); + } + + protected String getStylesheetContents() { + try { + URLConnection con = m_sheet.openConnection(); + + StringBuffer buffer = new StringBuffer(); + + String contentType = con.getContentType(); + + String encoding = "ISO-8859-1"; + int offset = (contentType == null ? -1 : contentType.indexOf( + "charset=")); + if (offset != -1) { + encoding = contentType.substring(offset + 8).trim(); + } + if (s_log.isDebugEnabled()) { + s_log.debug("Received content type " + contentType); + } + InputStream is = con.getInputStream(); + InputStreamReader isr = new InputStreamReader(is, encoding); + if (s_log.isDebugEnabled()) { + s_log.debug("Process with character encoding " + isr + .getEncoding()); + } + BufferedReader input = new BufferedReader(isr); + + String line; + while ((line = input.readLine()) != null) { + buffer.append(line).append('\n'); + } + input.close(); + return buffer.toString(); + + } catch (MalformedURLException ex) { + ex.printStackTrace(); + return "Stylesheet contents unavailable: " + ex.getMessage(); + } catch (IOException ex) { + ex.printStackTrace(); + return "Stylesheet contents unavailable: " + ex.getMessage(); + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/URL.java b/ccm-core/src/main/java/com/arsdigita/web/URL.java new file mode 100644 index 000000000..69ee16911 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/URL.java @@ -0,0 +1,990 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +import com.arsdigita.dispatcher.DispatcherHelper; +//import com.arsdigita.kernel.security.Util; +import com.arsdigita.util.Assert; +import com.arsdigita.util.servlet.HttpHost; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.log4j.Logger; +import org.libreccm.web.Application; + +/** + *

+ * URL models a future request according to the servlet worldview. Its principal + * uses are two: + * + *

    + *
  • To expose all the parts of a URL. To a servlet's way of thinking, these + * are the scheme, server name, server port, context path, servlet path, path + * info, and parameters.
  • + * + *
  • To generate URLs in a consistent and complete way in one place.
  • + *
+ *

+ * + *

+ * Each URL has the following accessors, here set next to an example URL + * instance, + * http://example.com:8080/ccmapp/forum/index.jsp?cat=2&cat=5:

+ * + *

+ * Atomic parts: + * + *

+ * getScheme() -> "http" + * getServerName() -> "example.com" + * getServerPort() -> 8080 + * getWebContextPath() -> "/ccmapp" + * getServletPath() -> "/forum" + * getPathInfo() -> "/index.jsp" + * getParameter("cat") -> "2" + * getParameterValues("cat") -> {"2", "5"} + *
+ * + *

+ * + *

+ * Composite parts: + * + *

+ * toString() -> "/ccmapp/forum/index.jsp?cat=2&cat=5" + * getURL() -> "http://example.com:8080/ccmapp/forum/index.jsp?cat=2&cat=5 + * getServerURI() -> "http://example.com:8080" // No trailing "/" + * getRequestURI() -> "/ccmapp/forum/index.jsp" + * getQueryString() -> "cat=2&cat=5" // No leading "?" + * getParameterMap() -> {cat={"2", "5"}} + *
+ * + *

+ * + *

+ * The toString() method returns a URL suitable for use in + * hyperlinks; since in the common case, the scheme, server name, and port are + * best left off, toString() omits them. The getURL() + * method returns a String URL which is fully qualified. Both + * getURL() and getServerURI() omit the port from + * their return values if the server port is the default, port 80.

+ * + *

+ * Creating URLs will usually be done via one of the static create methods:

+ * + *

+ * URL.root() creates a URL pointing at the server's root path, + * "/".

+ * + *

+ * URL.request(req, params) creates a URL reflecting the request + * the client made but using the passed-in parameters instead.

+ * + *

+ * URL.there(req, path, params) and its variants produce URLs that + * go through the CCM main dispatcher. The variant + * URL.there(req, app, pathInfo, params) dispatches to + * pathInfo under the specified application. The variant + * URL.here(req, pathInfo, params) dispatches to + * pathInfo under the current application.

+ * + *

+ * URL.excursion(req, path, params) produces URLs that go through + * the dispatcher to a destination but also encode and store the origin. This is + * used by LoginSignal and ReturnSignal to implement + * UI excursions.

+ * + *

+ * All static create methods taking an HttpServletRequest (1) + * preserve the request's scheme, server name, and port and (2) run parameter + * listeners if the URL's parameter map is not null. + *

+ * + *

+ * Those methods not taking an HttpServletRequest use the scheme, + * server name, and port defined in WebConfig.

+ * + *

+ * All static create methods taking a ParameterMap take null to + * mean no query string at all. URLs defined this way will have no query string + * and no "?".

+ * + *

+ * Those methods not taking a ParameterMap argument implicitly + * create an empty parameter map. Note that this is different from creating a + * URL with a null parameter map, which produces a URL with no query string.

+ * + * @see com.arsdigita.web.ParameterMap + * @see com.arsdigita.web.DispatcherServlet + * @see com.arsdigita.web.LoginSignal + * @see com.arsdigita.web.ReturnSignal + * @see com.arsdigita.web.WebConfig + * @see com.arsdigita.web.Application + * @author Justin Ross + * <jross@redhat.com> + * @version $Id$ + */ +public class URL { + + /** + * Internal logger instance to faciliate debugging. Enable logging output by + * editing /WEB-INF/conf/log4j.properties int hte runtime environment and + * set com.arsdigita.web.URL=DEBUG by uncommenting or adding the line. + */ + private static final Logger s_log = Logger.getLogger(URL.class); + + public static final String THEMES_DIR = "/themes"; + + /** + * Base direcotry for template files provided by packages. Each package has + * to place files into a subdirectory with its name + */ + public static final String TEMPLATE_DIR = "/templates"; + + /** + * The standard location for servlets. + */ + public static final String SERVLET_DIR = "/templates/servlet"; + + public static final String INTERNAL_THEME_DIR = THEMES_DIR + "/heirloom"; + + private static final ThreadLocal s_empty = new EmptyParameterMap(); + private StringBuffer m_url; + private ParameterMap m_params; + private int m_schemeEnd = -1; + private int m_serverNameEnd = -1; + private int m_serverPortEnd = -1; + private int m_contextPathEnd = -1; + private int m_servletPathEnd = -1; + private int m_dispatcherPrefixEnd = -1; + + private void init(final String scheme, + final String serverName, + final int serverPort, + final String contextPath, + final String servletPath, + final String dispatcherPrefix, + final String pathInfo, + final ParameterMap params) { + m_url = new StringBuffer(96); + m_params = params; + + if (Assert.isEnabled()) { + Assert.exists(scheme, "String scheme"); + Assert.isTrue(!scheme.equals(""), + "The scheme cannot be an empty string"); + + Assert.exists(serverName, "String serverName"); + Assert.isTrue(serverPort > 0, + "The serverPort must be greater than 0; " + "I got " + + serverPort); + + Assert.exists(contextPath, "String contextPath"); + + if (contextPath.startsWith("/")) { + Assert.isTrue(!contextPath.endsWith("/"), + "A contextPath starting with '/' must not end in '/'; " + + "I got '" + contextPath + "'"); + } + + Assert.exists(servletPath, "String servletPath"); + + if (pathInfo != null) { + Assert.isTrue(pathInfo.startsWith("/"), + "I expected a pathInfo starting with '/' " + + "and got '" + pathInfo + "' instead"); + } + } + + m_url.append(scheme); + m_schemeEnd = m_url.length(); + + m_url.append("://"); + + m_url.append(serverName); + m_serverNameEnd = m_url.length(); + + if (serverPort != 80) { + m_url.append(':'); + m_url.append(serverPort); + } + + m_serverPortEnd = m_url.length(); + + m_url.append(contextPath); + m_contextPathEnd = m_url.length(); + + if (dispatcherPrefix != null) { + m_url.append(dispatcherPrefix); + } + + m_dispatcherPrefixEnd = m_url.length(); + + m_url.append(servletPath); + m_servletPathEnd = m_url.length(); + + if (pathInfo != null) { + m_url.append(pathInfo); + } + + if (Assert.isEnabled()) { + Assert.isTrue(m_schemeEnd > -1); + Assert.isTrue(m_serverNameEnd > -1); + Assert.isTrue(m_serverPortEnd > -1); + Assert.isTrue(m_contextPathEnd > -1); + Assert.isTrue(m_servletPathEnd > -1); + } + } + + /** + *

+ * Assembles a fully qualified URL from its fundamental pieces. The contract + * of URL dictates that once params is passed in to this + * constructor, no parameters should be added or removed. This is to make + * URL in practice a read-only object.

+ * + * @param scheme "http", for example; see {@link + * javax.servlet.ServletRequest#getScheme()} + * + * @param serverName a valid domain name, for example + * "ccm.redhat.com"; see {@link + * javax.servlet.ServletRequest#getServerName()} + * + * @param serverPort 8080, for instance; see {@link + * javax.servlet.ServletRequest#getServerPort()} + * + * @param contextPath the path to your web app; empty string indicates the + * default context; any other values for contextPath must + * start with "/" but not end in + * "/"; contextPath cannot be null; see {@link + * javax.servlet.http.HttpServletRequest#getContextPath()} + * + * @param servletPath the path to your servlet; empty string and values + * starting with "/" are valid, but null is + * not; see {@link + * javax.servlet.http.HttpServletRequest#getServletPath()} + * + * @param pathInfo the path data remaining after the servlet path but + * before the query string; pathInfo may be null; see {@link + * javax.servlet.http.HttpServletRequest#getPathInfo()} + * + * @param params a ParameterMap representing a set of + * query parameters + * + * @return a fully specified URL + */ + public URL(final String scheme, + final String serverName, + final int serverPort, + final String contextPath, + final String servletPath, + final String pathInfo, + final ParameterMap params) { + HttpServletRequest req = Web.getRequest(); + String dispatcherPrefix = req == null ? null : DispatcherHelper. + getDispatcherPrefix(req); + + init(scheme, + serverName, + serverPort, + contextPath, + servletPath, + dispatcherPrefix, + pathInfo, + params); + } + + /** + * (private) Constructor. + * + * @param sreq + * @param params + */ + private URL(final HttpServletRequest sreq, + final ParameterMap params) { + final String dispatcherPrefix = DispatcherHelper.getDispatcherPrefix( + sreq); + final HttpHost host = new HttpHost(sreq); + + init(sreq.getScheme(), + host.getName(), + host.getPort(), + sreq.getContextPath(), + sreq.getServletPath(), + dispatcherPrefix, + sreq.getPathInfo(), + params); + } + + /** + *

+ * Constructor, produce a URL representation of the given request.

+ * + * @param sreq an HttpServletRequest from which to copy + * + * @return a URL whose contents correspond to the request used to create it + */ + public URL(final HttpServletRequest sreq) { + this(sreq, new ParameterMap(sreq)); + } + + /** + *

+ * Produces a short description of a URL suitable for debugging.

+ * + * @return a debugging representation of this URL + */ + public final String toDebugString() { + return super.toString() + " " + "[" + getScheme() + "," + + getServerName() + "," + getServerPort() + "," + + getContextPath() + "," + getServletPath() + "," + + getDispatcherPrefix() + "," + getPathInfo() + "," + + getQueryString() + "]"; + } + + /** + * Returns a String representation of the URL, fully qualified. + * The port is omitted if it is the standard HTTP port, 80. + * + * @return a String URL, with all of its parts + */ + public final String getURL() { + if (m_params == null) { + return m_url.toString(); + } else { + return m_url.toString() + m_params; + } + } + + /** + *

+ * Returns the scheme (sometimes called the protocol) of the URL. Examples + * are "http" and "https".

+ * + * @see javax.servlet.ServletRequest#getScheme() + * @return a String representing the URL's scheme + */ + public final String getScheme() { + return m_url.substring(0, m_schemeEnd); + } + + /** + *

+ * Returns the domain name part of the URL. For instance, + * "ccm.redhat.com".

+ * + * @see javax.servlet.ServletRequest#getServerName() + * @return a String representing the URL's server name + */ + public final String getServerName() { + return m_url.substring(m_schemeEnd + 3, m_serverNameEnd); + } + + /** + *

+ * Returns the port number of the URL. 8080, for example.

+ * + * @see javax.servlet.ServletRequest#getServerPort() + * @return an int for the URL's port number + */ + public final int getServerPort() { + final String port = m_url.substring(m_serverNameEnd, m_serverPortEnd); + + if (port.equals("")) { + return 80; + } else { + return Integer.parseInt(port.substring(1)); + } + } + + /** + *

+ * Returns the server half of the URL, as opposed to the "file" half. For + * example, "http://ccm.redhat.com:8080". Note that there is no trailing + * slash; any characters following the server port are considered part of + * the {@link #getRequestURI() request + * URI}.

+ * + *

+ * This method has no equivalent in the Servlet API, but it is similar in + * spirit to {@link + * javax.servlet.http.HttpServletRequest#getRequestURI()}.

+ * + *

+ * It is defined to return + * + *

getScheme() + "://" + getServerName() + ":" + * + getServerPort()
+ * + * or, if the server port is 80, + * + *
getScheme() + "://" + + * getServerName()
+ * + *

+ * + * @see #getRequestURI() + * @return a String comprised of the scheme, server name, and + * server port plus connecting bits + */ + public final String getServerURI() { + return m_url.substring(0, m_serverPortEnd); + } + + /** + *

+ * Returns the context path of the URL. The value cannot be null, and values + * starting with "/" do not end in "/"; empty + * string is a valid return value that stands for the default web app. + * Example values are "" and "/ccm-app".

+ * + * @see javax.servlet.http.HttpServletRequest#getContextPath() + * @return a String path to a web app context + */ + public final String getContextPath() { + return m_url.substring(m_serverPortEnd, m_contextPathEnd); + } + + /** + *

+ * Experimental

+ *

+ * Returns the dispatcher prefix of this request as set by the + * InternalPrefixerServlet + */ + public final String getDispatcherPrefix() { + if (m_dispatcherPrefixEnd < m_servletPathEnd) { + //there is no dispatcher prefix + return ""; + } else { + return m_url.substring(m_servletPathEnd, m_dispatcherPrefixEnd); + } + } + + /** + *

+ * Returns the servlet path of the URL. The value cannot be null.

+ * + * @see javax.servlet.http.HttpServletRequest#getServletPath() + * @return a String path to a servlet + */ + public final String getServletPath() { + return m_url.substring(m_dispatcherPrefixEnd, m_servletPathEnd); + } + + /** + *

+ * Returns the servlet-local path data of the URL. The value may be null. If + * it is not null, the value begins with a "/". Examples are + * null, "/", and "/remove.jsp".

+ * + * @see javax.servlet.http.HttpServletRequest#getPathInfo() + * @return a String of path data addressed to a servlet + */ + public final String getPathInfo() { + final String pathInfo = m_url.substring(m_servletPathEnd); + + if (pathInfo.equals("")) { + return null; + } else { + return pathInfo; + } + } + + /** + *

+ * Returns the "file" part of the URL, in contrast to the + * {@link #getServerURI() server part}. The value cannot be null and always + * starts with a "/". For example, "/ccm/forum/thread.jsp".

+ * + *

+ * This method is defined to return the equivalent of * getWebContextPath() + getServletPath() + + getPathInfo().

+ * + * @see javax.servlet.http.HttpServletRequest#getRequestURI() + * @return a String comprised of the context path, servlet + * path, and path info + */ + public final String getRequestURI() { + return m_url.substring(m_serverPortEnd); + } + + /** + *

+ * Returns the query string of the URL. If the URL was constructed with a + * null ParameterMap, this method returns null. If the URL was + * constructed with an empty ParameterMap, this method returns + * the empty string. Example values are null, "", + * and "ticket-id=56&user-id=24".

+ * + * @see javax.servlet.http.HttpServletRequest#getQueryString() + * @return a String representing the query parameters of the + * URL + */ + public final String getQueryString() { + if (m_params == null) { + return null; + } else { + return m_params.getQueryString(); + } + } + + /** + *

+ * Returns the value of one query parameter. If the URL was constructed with + * a null ParameterMap, this method returns null. If the + * parameter requested has multiple values, this method will only return the + * first; use {@link + * #getParameterValues(String)} to get all of the values.

+ * + * @see javax.servlet.http.HttpServletRequest#getParameter(String) + * @param name the name of the parameter to fetch + * + * @return the String value of the parameter + */ + public final String getParameter(final String name) { + if (m_params == null) { + return null; + } else { + return m_params.getParameter(name); + } + } + + /** + *

+ * Returns the values for a parameter. If the URL was constructed with a + * null ParameterMap, this method returns null.

+ * + * @see javax.servlet.http.HttpServletRequest#getParameterValues(String) + * @param name the name of the parameter to get + * + * @return a String[] of values for the parameter + */ + public final String[] getParameterValues(final String name) { + if (m_params == null) { + return null; + } else { + return m_params.getParameterValues(name); + } + } + + /** + *

+ * Returns an immutable map of the query parameters. The map's keys are + * Strings and the map's values are String[]s. If + * the URL was constructed with a null ParameterMap, this + * method returns null.

+ * + * @see javax.servlet.http.HttpServletRequest#getParameterMap() + * @return a Map of the URL's query parameters + */ + public final Map getParameterMap() { + if (m_params == null) { + return null; + } else { + return m_params.getParameterMap(); + } + } + + /** + *

+ * Creates a URL to the site's root path. For example, + * http://somewhere.net/.

+ * + * @return a URL to your server's root path + */ + public static final URL root() { + final WebConfig config = Web.getConfig(); + + URL url = new URL(config.getDefaultScheme(), + config.getServer().getName(), + config.getServer().getPort(), + "", + "/", + null, + null); + + return url; + } + + /** + *

+ * Creates a URL using the elements of the user's original request but with + * the given set of parameters instead of the original ones.

+ * + * @param sreq the servlet request + * @param params a ParameterMap of params to replace those of + * the request + * + * @return a URL representing the original request except for + * its parameters + */ + public static final URL request(final HttpServletRequest sreq, + final ParameterMap params) { + if (params != null) { + params.runListeners(sreq); + } + + final URL url = Web.getWebContext().getRequestURL(); + + if (url == null) { + // If the URL is being generated outside of a WebContext, + // use the request to fill out the URL. + + return new URL(sreq, params); + } else { + return new URL(url.getScheme(), + url.getServerName(), + url.getServerPort(), + url.getContextPath(), + url.getServletPath(), + url.getPathInfo(), + params); + } + } + + /** + *

+ * Creates a URL to path under the CCM main dispatcher and with + * the given parameters. A null ParameterMap indicates that the + * URL has no query string at all. If the parameter map is not null, its + * parameter listeners are run and may further edit the parameter map.

+ * + * @see com.arsdigita.web.DispatcherServlet + * @param sreq the servlet request + * @param path a String path to which to dispatch + * @param params a ParameterMap of parameters to use; this + * value may be null + * + * @return a URL with a path to dispatch to + */ + public static final URL there(final HttpServletRequest sreq, + final String path, + final ParameterMap params) { + final WebConfig config = Web.getConfig(); + + Assert.exists(sreq, "HttpServletRequest sreq"); + Assert.exists(config, "WebConfig config"); + + if (params != null) { + params.runListeners(sreq); + } + + final HttpHost host = new HttpHost(sreq); + + return new URL(sreq.getScheme(), + host.getName(), + host.getPort(), + config.getDispatcherContextPath(), + config.getDispatcherServletPath(), + path, + params); + } + + /** + * Method similar to there(), but which checks the + * waf.web.dynamic_host_provider parameter to generate the site name and + * port dynamically. + * + * @see com.arsdigita.web.DispatcherServlet + * @param sreq the servlet request + * @param path a String path to which to dispatch + * @param params a ParameterMap of parameters to use; this + * value may be null + * + * @return a URL with a path to dispatch to + */ + public static final URL dynamicHostThere(final HttpServletRequest sreq, + final String path, + final ParameterMap params) { + final WebConfig config = Web.getConfig(); + DynamicHostProvider provider = Web.getConfig().getDynamicHostProvider(); + + if (provider == null) { + return there(sreq, path, params); + } + + Assert.exists(sreq, "HttpServletRequest sreq"); + Assert.exists(config, "WebConfig config"); + + if (params != null) { + params.runListeners(sreq); + } + + final HttpHost host = new HttpHost(sreq); + + return new URL(sreq.getScheme(), + provider.getName(), + provider.getPort(), + config.getDispatcherContextPath(), + config.getDispatcherServletPath(), + path, + params); + } + + /** + *

+ * Creates a URL with no local parameters to path under the CCM + * main dispatcher. This method implicitly creates an empty parameter map + * (not a null one); this empty map may be altered by parameter listeners, + * for instance to include global parameters.

+ * + * @param sreq the servlet request + * @param path a String path to dispatch to + * + * @return a URL to a path under the dispatcher and with an + * empty parameter map + */ + public static final URL there(final HttpServletRequest sreq, + final String path) { + final WebConfig config = Web.getConfig(); + + Assert.exists(sreq, "HttpServletRequest sreq"); + Assert.exists(config, "WebConfig config"); + + final HttpHost host = new HttpHost(sreq); + + return new URL(sreq.getScheme(), + host.getName(), + host.getPort(), + config.getDispatcherContextPath(), + config.getDispatcherServletPath(), + path, + (ParameterMap) s_empty.get()); + } + + /** + *

+ * Creates a URL to pathInfo under the specified application + * and using the given parameters. The parmeter map argument may be null, + * indicating that the URL has no query string.

+ * + * @param sreq the servlet request + * @param app the Application to dispatch to + * @param pathInfo a String of extra path info for the + * application + * @param params a ParameterMap of parameters to use + * + * @return a URL to an application with a particular + * pathInfo + */ + public static final URL there(final HttpServletRequest sreq, + final Application app, + final String pathInfo, + final ParameterMap params) { + if (Assert.isEnabled() && pathInfo != null) { + Assert.isTrue(pathInfo.startsWith("/"), + "pathInfo, if not null, must " + "start with a slash"); + } + + if (pathInfo == null) { + return URL.there(sreq, app.getPrimaryUrl().toString(), params); + } else { + return URL.there(sreq, app.getPrimaryUrl().toString() + pathInfo, + params); + } + } + + /** + *

+ * Creates a URL with no local parameters to pathInfo under the + * specified application. + * + * @param sreq the servlet request + * @param app the Application to dispatch to + * @param pathInfo a String of extra path info for the + * application + * + * @return a URL to an application with a particular + * pathInfo + */ + public static final URL there(final HttpServletRequest sreq, + final Application app, + final String pathInfo) { + if (Assert.isEnabled() && pathInfo != null) { + Assert.isTrue(pathInfo.startsWith("/"), + "pathInfo, if not null, must " + "start with a slash"); + } + + if (pathInfo == null) { + return URL.there(sreq, app.getPrimaryUrl().toString()); + } else { + return URL.there(sreq, app.getPrimaryUrl().toString() + pathInfo); + } + } + + /** + *

+ * Creates a URL with local parameters.

+ * + *

+ * This function should not be used unless you really don't have an + * HttpServletRequest object as it will ignore any Host header + * given by the client.

+ * + * @param path + * @param params + * + * @return + */ + public static final URL there(final String path, + final ParameterMap params) { + final WebConfig config = Web.getConfig(); + + return new URL(config.getDefaultScheme(), + config.getServer().getName(), + config.getServer().getPort(), + "", + config.getDispatcherServletPath(), + path, + params); + } + + /** + *

+ * Create a URL with local parameters to pathInfo under the + * specified application.

+ * + *

+ * This function should not be used unless you really don't have an + * HttpServletRequest object as it will ignore any Host header + * given by the client.

+ */ + public static final URL there(final Application app, + final String pathInfo, + final ParameterMap params) { + return URL.there(app.getPrimaryUrl() + pathInfo, params); + } + + public static final URL here(final HttpServletRequest sreq, + final String pathInfo, + final ParameterMap params) { + final Application app = Web.getWebContext().getApplication(); + + Assert.exists(app, "Application app"); + + return URL.there(sreq, app, pathInfo, params); + } + + public static final URL here(final HttpServletRequest sreq, + final String pathInfo) { + final Application app = Web.getWebContext().getApplication(); + + Assert.exists(app, "Application app"); + + return URL.there(sreq, app, pathInfo); + } + + public static URL excursion(final HttpServletRequest sreq, + final String path, + final ParameterMap params) { + if (s_log.isDebugEnabled()) { + s_log.debug("Creating excursion URL to " + path); + } + + final URL url = URL.there(sreq, path, params); + + params.setParameter("return_url", Web.getWebContext().getRequestURL()); + + return url; + } + + public static URL excursion(final HttpServletRequest sreq, + final String path) { + return URL.excursion(sreq, path, new ParameterMap()); + } + + static URL login(final HttpServletRequest sreq) { + //Replace register eventuelly... + return URL.excursion(sreq, + "/register", + (ParameterMap) s_empty.get()); + } + + final String getReturnURL() { + return getParameter("return_url"); + } + + /** + * Returns a String representation of the URL suitable for use + * as a hyperlink. The scheme, server name, and port are omitted. + * + * @return a String URL + */ + @Override + public final String toString() { + if (m_params == null) { + return m_url.substring(m_serverPortEnd); + } else { + String str = m_url.substring(m_serverPortEnd); + if (str.contains("?")) { + return String.format("%s%s", m_url.substring(m_serverPortEnd), + m_params.toString().replace('?', '&')); + } else { + return m_url.substring(m_serverPortEnd) + m_params; + } + } + } + + /** + * + * @return + */ + public static String getDispatcherPath() { + final WebConfig config = Web.getConfig(); + final HttpServletRequest req = Web.getRequest(); + + final String context = config.getDispatcherContextPath(); + final String servlet = config.getDispatcherServletPath(); + + if (req == null) { + return context + servlet; + } else { + final String prefix = DispatcherHelper.getDispatcherPrefix(req); + + if (prefix == null) { + return context + servlet; + } else { + return context + prefix + servlet; + } + } + } + + private static class EmptyParameterMap extends InternalRequestLocal { + + @Override + protected final Object initialValue() { + return new ParameterMap(); + } + + @Override + protected final void prepareValue(final HttpServletRequest sreq) { + ((ParameterMap) get()).runListeners(sreq); + } + + @Override + protected final void clearValue() { + ((ParameterMap) get()).clear(); + } + + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/Web.java b/ccm-core/src/main/java/com/arsdigita/web/Web.java new file mode 100644 index 000000000..d9b28505e --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/Web.java @@ -0,0 +1,777 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.arsdigita.web; + +import com.arsdigita.util.Assert; +import com.arsdigita.util.StringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; + +import org.apache.log4j.Logger; +import org.libreccm.core.CcmSessionContext; + +/** + * An entry point for functions of the web package. + * + * @author Rafael Schloming <rhs@mit.edu> + * @author Justin Ross <jross@redhat.com> + * @version $Id$ + */ +public class Web { + + /** + * Internal logger instance to faciliate debugging. Enable logging output by + * editing /WEB-INF/conf/log4j.properties int the runtime environment and + * set com.arsdigita.web.Web=DEBUG by uncommenting or adding the line. + */ + private static final Logger s_log = Logger.getLogger(Web.class); + + private static final WebConfig s_config = WebConfig.getInstanceOf(); + + private static final ThreadLocal s_request = new InternalRequestLocal(); + private static final ThreadLocal s_servletContext + = new InternalRequestLocal(); + private static final ThreadLocal s_userContext = new InternalRequestLocal(); + private static ThreadLocal s_context; + + static final WebContext s_initialContext = new WebContext(); + + /** + * Internal service property to temporarly save the ServletContext as + * determined by findResource(resource) method to make it available to those + * methods of this class which use findResource to lookup a resource as a + * base for determining additional information, e.g. provide a dispatcher + * (findResourceDispatcher) + */ + static private ServletContext s_urlContext; + + /** + * String containing the webapp context path portion of the WEB application + * where this CCM instance is executed. (I.e. where the WEB-INF directory is + * located in the servlet container webapps directory). + */ + private static String s_contextPath; + + /** + * Static Initializer block. + */ + static void init(final HttpServletRequest sreq, + final ServletContext sc, + final CcmSessionContext uc) { + + Assert.exists(sreq, HttpServletRequest.class); + Assert.exists(sc, ServletContext.class); + Assert.exists(uc, CcmSessionContext.class); + + s_request.set(sreq); + s_servletContext.set(sc); + s_contextPath = CCMDispatcherServlet.getContextPath(); + s_userContext.set(uc); + } + + /** + * Provide the configuration record for code in the web package. + * + * @return A WebConfig configuration record; it cannot be null + */ + public static WebConfig getConfig() { + return s_config; + } + + /** + * Gets the web context object from the current thread. + * + * @return A WebContext object; it cannot be null Note: Rename + * from getContext() + */ + public static WebContext getWebContext() { + if (s_context == null) { + s_context = new WebContextLocal(); + } + return (WebContext) s_context.get(); + } + + /** + * Gets the servlet request object of the current thread. + * + * @return The current HttpServletRequest; it can be null + */ + public static HttpServletRequest getRequest() { + return (HttpServletRequest) s_request.get(); + } + + /** + * Gets the servlet context of the current thread. + * + * @return The current ServletContext; it can be null + */ + public static ServletContext getServletContext() { + return (ServletContext) s_servletContext.get(); + } + + /** + * Gets the user context object of the current thread. + * + * @return The current UserContext object; it can be null + */ + public static CcmSessionContext getUserContext() { + return (CcmSessionContext) s_userContext.get(); + } + + /** + * Gets the webapp context path portion of the WEB application where this + * CCM instance is executed. (I.e. where the WEB-INF directory is located + * in the servlet container webapps directory, known as ServletContext in + * the Servlet API) + * + * @return web context path portion as a String, may be used to construct + * a URL (NOT the RealPath!). The ROOT context returns an empty + * String(""). + */ + public static String getWebappContextPath() { + return (String) s_contextPath; + } + + /** + * Sets the webapp context path portion of the WEB application where this + * CCM instance is executed. (I.e. where the WEB-INF directory is located + * in the servlet container webapps directory, known as ServletContext in + * the Servlet API) + * Meant to be executed by CCMDispatcherServlet only. + * + * @param contextPath + */ + protected static void setWebappContextPath(String contextPath) { + s_contextPath = contextPath; + } + + + /** + * Processes an URL String trying to identify a corresponding recource which + * is mapped to the given path String. The method ensures that the resource + * definitely exists (using the URL returned) or definitely not (returning + * null). + * + * The resourcePath may be stored at various sources (file system, jar file, + * database, etc) depending on the implementation of the URL handlers and + * URLConnection objects. + * + * + * @param resourcePath Path to the resource as String. It may include the + * web context in its first part or may be relative to + * the current webapp document root (i.e. its context). + * Additionally, the web application component (if any) + * may be a comma separate list of webapps to search for + * the rest of the path String. So, if the + * 'resourcePath' is:
+     *                 /myproj,ccm-cms/themes/heirloom/admin/index.xsl
+     *                     
then this method will look for resourcePaths at + *
+     *                 /myproj/themes/heirloom/admin/index.xsl
+     *                 /ccm-cms/themes/heirloom/admin/index.xsl
+     * 
+ * + * @return the URL for the resourcePath, or null if no resource is mapped to + * the resourcePath String + */ + public static URL findResource(String resourcePath) { + + if (resourcePath == null) { + if (s_log.isDebugEnabled()) { + s_log.debug("Parameter resource is null. Giving up."); + } + return null; + } + // ensure a leading "/" + if (!resourcePath.startsWith("/")) { + resourcePath = "/" + resourcePath; + } + if (resourcePath.length() < 2) { + if (s_log.isDebugEnabled()) { + s_log + .debug("Resource spec is too short: >" + resourcePath + "<"); + } + return null; + } + + // determine my own webapp context + ServletContext myctx = getServletContext(); + + // Check for old style resourcePath format including a comma seoarated list + // of webapps + if (resourcePath.indexOf(",") <= 0) { + // no comma separated list found, process as normal + + // just try to find the resourcePath in my own context + try { + URL url = myctx.getResource(resourcePath); + if (url != null) { + if (s_log.isDebugEnabled()) { + s_log.debug("Got URL " + url + " for " + resourcePath); + } + return url; // Return adjusted resourcePath url + } + } catch (MalformedURLException ex) { + if (s_log.isDebugEnabled()) { + s_log.debug("Cannot get resource for " + resourcePath); + } + // Try the first part of resourcePath as a webapp context path and + // check far a resourcePath there + int offset = resourcePath.indexOf("/", 1); // search for second "/" + String testPath = resourcePath.substring(1, offset); + String path = resourcePath.substring(offset); + + if (s_log.isDebugEnabled()) { + s_log.debug("Try to find a context at " + testPath); + } + // Try to achieve a context + ServletContext ctx = myctx.getContext(testPath); + if (s_log.isDebugEnabled()) { + s_log + .debug("Servlet context for " + testPath + " is " + ctx); + } + if (ctx != null) { + // successs, try to finf a resourcePath for the remaining + // string as path + try { + URL url = ctx.getResource(path); + if (url != null) { + if (s_log.isDebugEnabled()) { + s_log.debug("Got URL " + url + " for " + path); + } + return url; // Return adjusted resourcePath url + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("No URL present for " + path); + } + } + } catch (MalformedURLException exc) { + if (s_log.isDebugEnabled()) { + s_log.debug("cannot get resource for " + path); + } + } + } + } + + return null; // fall through + + } else { + // comma separated list found + // processing old style, comma separated webapp list + int offset = resourcePath.indexOf("/", 1); // search for second "/" + String webappList = resourcePath.substring(1, offset); + String path = resourcePath.substring(offset); + + String[] webapps = StringUtils.split(webappList, ','); + if (s_log.isDebugEnabled()) { + s_log.debug("Web app list " + webappList + " path " + path); + } + + for (int i = (webapps.length - 1); i >= 0; i--) { + + String ctxPath = webapps[i]; + if (!ctxPath.startsWith("/")) { + ctxPath = "/" + ctxPath; + } + // No trailing slash allowed by servlet API! + // if (!ctxPath.endsWith("/")) { + // ctxPath = ctxPath + "/"; + // } + + ServletContext ctx = myctx.getContext(ctxPath); + if (s_log.isDebugEnabled()) { + s_log.debug("Servlet context for " + ctxPath + " is " + ctx); + } + if (ctx != null) { + try { + URL url = ctx.getResource(path); + if (url != null) { + if (s_log.isDebugEnabled()) { + s_log.debug("Got URL " + url + " for " + path); + } + return url; // Return adjusted resourcePath url + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("No URL present for " + path); + } + } + } catch (MalformedURLException ex) { + if (s_log.isDebugEnabled()) { + s_log.debug("cannot get resource for " + path); + } + } + } + + } + + return null; // fall through when nothing found + } // end processing old style comma separated list + } + + /** + * Follows the same rules as findResource(String[], String), but instead + * returns an input stream for reading the resource + * + * @param resource Path to the resource as String. It may include the web + * context in its first part or may be relative to the + * current webapp document root (i.e. its context). + * Additionally, it the web application component (if any) + * may be a comma separate list of webapps to search for the + * rest of the path String. So, if the 'resource' is:
+     *                 /myproj,ccm-cms/themes/heirloom/admin/index.xsl
+     *                 
then this method will look for resources at + *
+     *                 /myproj/themes/heirloom/admin/index.xsl
+     *                 /ccm-cms/themes/heirloom/admin/index.xsl
+     * 
+ * + * @return the input stream for the resource, or null + * + * @throws java.io.IOException + */ + public static InputStream findResourceAsStream(String resource) + throws IOException { + + URL url = findResource(resource); + return url == null ? null : url.openStream(); + } + + /** + * Follows the same rules as findResource(String), but instead returns a + * request dispatcher for serving the resource. It is mainly used to find an + * application's jsp template(s) stored in the file system (or war file in + * case of unexploded distribution) and provide a handle to execute it. + * These jsp templates used to be stored a directory named "templates" and + * there within a directory carrying the modules name. As example: + * "/templates/ccm-navigation/index.jsp". Inside the modules subdirectory + * there might by a module specific subdirectory structure. It's up to the + * module. + * + * @param resourcePath Path to the resource as String. It may include the + * web context in its first part or may be relative to + * the current webapp document root (i.e. its context). + * LEGACY FORMAT: Additionally, the web application + * component (if any) may be a comma separate list of + * webapps to search for the rest of the path String. + * So, if the 'resource' is:
+     *                 /myproj,ccm-cms/themes/heirloom/admin/index.xsl
+     *                     
then this method will look for resources at + *
+     *                 /myproj/themes/heirloom/admin/index.xsl
+     *                 /ccm-cms/themes/heirloom/admin/index.xsl
+     * 
LEGACY FORMAT SUPPORT NOT IMPLEMENTED YET! LEGACY FORMAT MAY BE + * COMPLETELY REMOVED IN FUTURE RELEASE + * + * @return the request dispatcher for the resource, or null + */ + public static RequestDispatcher findResourceDispatcher(String resourcePath) { + + if (resourcePath == null) { + return null; + } + ServletContext ctx = getServletContext(); + URL url = null; + + // Check for old style resource format including a comma seoarated list + // of webapps + if (resourcePath.indexOf(",") <= 0) { + // no comma separated list found, process as normal + + try { + url = ctx.getResource(resourcePath); + } catch (MalformedURLException ex) { + if (s_log.isDebugEnabled()) { + s_log.debug("Resource for " + resourcePath + " not found."); + } + // throw new UncheckedWrapperException( + // "No resource at " + resourcePath, ex); + return null; + } + if (url == null) { + return null; + } else { + RequestDispatcher rd = (ctx == null) ? null : ctx + .getRequestDispatcher(resourcePath); + return rd; + } + + } else { + + // old style format not implemented yet here + return null; + + } + + } + + /** + * + */ + private static class WebContextLocal extends InternalRequestLocal { + + @Override + protected Object initialValue() { + return Web.s_initialContext.copy(); + } + + @Override + protected void clearValue() { + ((WebContext) get()).clear(); + } + + } + + // /////////////////////////////////////////////////////////////////////// + // + // DEPRECATED METHODS + // ================== + // This method assume the main ccm application installed in the hosts root + // context and somme other ccm applications installed in its own context. + // It assumes futher that each ccm applications registers itself in a + // home made application directory and it is viable to query this + // directory to find the context for a given ccm application. + // + // /////////////////////////////////////////////////////////////////////// + /** + * Constant to denote the context of the ROOT application (main CCM app). + * Used by some classes to determine the application context (in terms of + * servlet specification, i.e. document root of the web application where + * all the code, specifically WEB-INF, is copied into when unpacking the WAR + * file. + * + * This results in a fixed location (context) for CCM which is no longer + * valid. Replace by invoking method getWebappContextPath + * + * @deprecated without direct replacement. See above + */ +// private static final String ROOT_WEBAPP = "ROOT"; + /** + * Map containing a list of registered ccm webapps and corresponding webapp + * context (ServletContext in JavaEE terms). + * + * @deprecated without direct replacement, see above. + */ +// private static final Map s_contexts = new HashMap(); + /** + * @deprecated renamed to getWebContext + */ +// getContext() + /** + * Gets the servlet context matching the provided URI. The URI is relative + * to the root of the server and must start and end with a '/'. It is + * provided by ContextRegistrationServlet as manually configured in web.xml + * + * This should be used in preference to ServletContext#getWebContext(String) + * since on all versions of Tomcat, this fails if the path of the context + * requested is below the current context. + * + * @param uri the context URI + * + * @return the servlet context matching uri, or null + * + * @deprecated currently without direct replacement The hash map s_contexts + * contains a kind of repository where (i.e. in which web application + * context) a resource may be found. Part of the code access the file system + * directly which is normally forbidden and violates the principle of + * isolation Previously it has been used to allow the installation of some + * modules in its own web application context (e.g. Themedirector) where + * each module used to register here via ContextRegistrationServlet. This + * mechanism has to be replaced by a inter-web-application communication, if + * modules should be enabled to execute in it's web application context. + * + */ +// public static ServletContext getServletContext(String uri) { +// Assert.isTrue(uri.startsWith("/"), "uri must start with /"); +// Assert.isTrue(uri.endsWith("/"), "uri must end with /"); +// return (ServletContext)s_contexts.get(uri); +// } + /** + * Registers a servlet context against a URI. Only intended to be used by + * ContextRegistrationServlet + * + * @deprecated without direct replacement. See getServletContext + */ +// static final void registerServletContext(String uri, +// ServletContext ctx) { +// s_log.debug("Mapping " + ctx + " to " + uri); +// Assert.isTrue(s_contexts.get(uri) == null, +// "a context mapping exists at " + uri); +// // Save the web context as manually configured in web.xml +// // along with the context as provided by ServletContext. +// s_contexts.put(uri, ctx); +// } + /** + * Unregisters the servlet context against a URI. Only intended to be used + * by ContextRegistrationServlet + * + * @deprecated without direct replacement. See getServletContext + */ +// static final void unregisterServletContext(String uri) { +// s_log.debug("Unmapping " + uri); +// s_contexts.remove(uri); +// } + /** + * Finds a concrete URL corresponding to an abstract webapp resource. The + * first argument is a list of webapp paths to search through for the path. + * So if the webapps param is { 'myproj', 'ccm-cms', 'ROOT' } and the path + * parma is '/themes/heirloom/apps/content-section/index.xsl' then the paths + * that are searched are: + *
+     *  /myproj/themes/heirloom/apps/content-section/index.sl
+     *  /ccm-cms/themes/heirloom/apps/content-section/index.sl
+     *  /ROOT/themes/heirloom/apps/content-section/index.sl
+     * 
+ * + * @param webapps the list of webapps + * @param path the resource path + * + * @return the URL for the resource, or null + * + * @deprecated without direct replacement at the moment. + */ +// public static URL findResource(String[] webapps, +// String path) { +// +// ServletContext ctx = findResourceContext(webapps, +// path); +// +// URL url = null; +// try { +// url = (ctx == null ? null : +// ctx.getResource(path)); +// } catch (IOException ex) { +// throw new UncheckedWrapperException("cannot get URL for " + path, ex); +// } +// if (s_log.isDebugEnabled()) { +// s_log.debug("URL for " + path + " is " + url); +// } +// return url; +// } + /** + * Follows the same rules as findResource(String), but instead returns an + * input stream for reading the resource + * + * @param resource the resource name + * + * @return the input stream for the resource, or null + * + * @deprecated without direct replacement at the moment. + */ +// public static InputStream findResourceAsStream(String resource) +// throws IOException { +// ResourceSpec spec = parseResource(resource); +// +// return findResourceAsStream(spec.getWebapps(), +// spec.getPath()); +// } + /** + * Follows the same rules as findResource(String[], String), but instead + * returns an input stream for reading the resource + * + * @param webapps the list of webapps + * @param path the resource path + * + * @return the input stream for the resource, or null + * + * @deprecated without direct replacement at the moment. + */ +// public static InputStream findResourceAsStream(String[] webapps, +// String path) +// throws IOException { +// +// URL url = findResource(webapps, path); +// +// return url == null ? null : +// url.openStream(); +// } + /** + * Follows the same rules as findResource(String), but instead returns a + * request dispatcher for serving the resource + * + * @param resource the resource name + * + * @return the request dispatcher for the resource, or null + * + * @deprecated without direct replacement at the moment. + */ +// public static RequestDispatcher findResourceDispatcher(String resource) { +// ResourceSpec spec = parseResource(resource); +// +// return findResourceDispatcher(spec.getWebapps(), +// spec.getPath()); +// } +// /** +// * Follows the same rules as findResource(String[], String), but +// * instead returns a request dispatcher for serving +// * the resource +// * +// * @param webapps the list of webapps +// * @param path the resource path +// * @return the request dispatcher for the resource, or null +// * @deprecated without direct replacement at the moment. +// */ +// public static RequestDispatcher findResourceDispatcher(String[] webapps, +// String path) { +// ServletContext ctx = findResourceContext(webapps, +// path); +// +// return ctx == null ? null : ctx.getRequestDispatcher(path); +// } + /** + * + * @param webapps + * @param path path to the resource, starting with "/" and relative to + * the current context root, or relative to the + * /META-INF/resources directory of a JAR file inside the web + * application's /WEB-INF/lib directory + * + * @return + * + * @deprecated without direct replacement at the moment. + */ +// private static ServletContext findResourceContext(String[] webapps, +// String path) { +// for (int i = (webapps.length - 1) ; i >= 0 ; i--) { +// // trash here, depends of a kind of "home made" list of +// // webapps/webcontexts (or ServletContexts) which are part of CCM +// // but installed in its own context (it is the structure of APLAWS +// // until 1.0.4. +// String ctxPath = ROOT_WEBAPP.equals(webapps[i]) ? +// "" : webapps[i]; +// +// if (!ctxPath.startsWith("/")) { +// ctxPath = "/" + ctxPath; +// } +// if (!ctxPath.endsWith("/")) { +// ctxPath = ctxPath + "/"; +// } +// +// ServletContext ctx = getServletContext(ctxPath); +// if (s_log.isDebugEnabled()) { +// s_log.debug("Servlet context for " + ctxPath + " is " + ctx); +// } +// +// if (ctx != null) { +// try { +// URL url = ctx.getResource(path); +// if (url != null) { +// if (s_log.isDebugEnabled()) { +// s_log.debug("Got URL " + url + " for " + path); +// } +// return ctx; +// } else { +// if (s_log.isDebugEnabled()) { +// s_log.debug("No URL present for " + path); +// } +// } +// } catch (IOException ex) { +// throw new UncheckedWrapperException( +// "cannot get resource " + path, ex); +// } +// } +// } +// return null; +// } + // ///////////////////////////////////////////////////////////////////////// + // Private classes and methods + // ///////////////////////////////////////////////////////////////////////// + /** + * Splits the resource string into a StringArray of webapps (ServletContexts + * in terms of JavaEE) and a path to a resource inside a the that webapp. + * The part between the first and the second slash is always treated as + * webapp part! This part may consist of a comma separated list in which + * case the result is an array of webapps > 1. + * + * As of version 6.6x CCM is installed into one webapp context by default + * and the assumption of the first part being a web app is nolonger + * reloiable. Therefore this routine provides invalid results in some + * circumstances! In best cases it provides redundancy specifying just the + * local webapp. + * + * Code may be refactored to ensure the first part is really a webapp by + * inquiring the servlet container using javax.management + * + * @param resource + * + * @return + * + * @deprecated without direct replacement. + */ +// private static ResourceSpec parseResource(String resource) { +// if (resource == null || resource.length() < 2) { +// throw new IllegalArgumentException( +// "Resource spec is too short: " + resource); +// } +// +// int offset = resource.indexOf("/", 1); +// if (offset == -1) { +// throw new IllegalArgumentException( +// "Cannot find second '/' in resource spec : " + resource); +// } +// +// String webappList = resource.substring(1, offset); +// String path = resource.substring(offset); +// +// String[] webapps = StringUtils.split(webappList, ','); +// +// if (s_log.isInfoEnabled()) { +// s_log.info("Web app list " + webappList + " path " + path); +// } +// +// return new ResourceSpec(webapps, path); +// } +// +// +// /** +// * Container to hold a pointer to a resource. The pointer specifically +// * consists of an array of webapps probably containing the requested +// * resource and a path to that resource that has to be equal for each +// * webapp. +// * @deprecated without direct replacement at the moment. +// */ +// private static class ResourceSpec { +// private final String[] m_webapps; +// private final String m_path; +// +// /** +// * Constructor. +// * @param webapps +// * @param path +// */ +// public ResourceSpec(String[] webapps, +// String path) { +// m_webapps = webapps; +// m_path = path; +// } +// +// public String[] getWebapps() { +// return m_webapps; +// } +// +// public String getPath() { +// return m_path; +// } +// } +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/WebConfig.java b/ccm-core/src/main/java/com/arsdigita/web/WebConfig.java new file mode 100644 index 000000000..a845b3a5c --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/WebConfig.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2015 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package com.arsdigita.web; + +import com.arsdigita.runtime.AbstractConfig; +import com.arsdigita.util.parameter.BooleanParameter; +import com.arsdigita.util.parameter.EnumerationParameter; +import com.arsdigita.util.parameter.ErrorList; +import com.arsdigita.util.parameter.Parameter; +import com.arsdigita.util.parameter.ParameterError; +import com.arsdigita.util.parameter.SingletonParameter; +import com.arsdigita.util.parameter.StringArrayParameter; +import com.arsdigita.util.parameter.StringParameter; +import com.arsdigita.util.servlet.HttpHost; +import com.arsdigita.util.servlet.HttpHostParameter; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A record containing server-session scoped configuration properties. + * + * Accessors of this class may return null. Developers should take care to trap + * null return values in their code. + * + * @see com.arsdigita.web.Web + * @author Justin Ross <jross@redhat.com> + * @author Jens Pelzetter + */ +public class WebConfig extends AbstractConfig { + + private static final Logger LOGGER = LogManager.getLogger(WebConfig.class); + + private static WebConfig config; + + /** + * Returns the singleton configuration record for the content section + * environment. + * + * @return The CMSConfig record; it cannot be null + */ + public static synchronized WebConfig getInstanceOf() { + if (config == null) { + config = new WebConfig(); + config.load(); + } + return config; + } + + // ///////////////////////////////////////////////////////////////////////// + // Configuration parameter section + // ///////////////////////////////////////////////////////////////////////// + /** + * Determines what HTTP scheme prefix is used by default to generate URLs + * (either http od https) + */ + private final Parameter m_scheme = new DefaultSchemeParameter( + "waf.web.default_scheme", + Parameter.REQUIRED, "http"); + /** + * Sets the name and port that users of a site will see in URLs generated by + * CCM for the site. This is a required parameter during installation, e.g. + * example.com:80 + */ + private final Parameter m_server = new HttpHostParameter("waf.web.server"); + /** + * Name and port that users of a site will see in secure URLs generated by + * CCM for the site. As an example: example.com:443 + */ + private final Parameter m_secureServer = new HttpHostParameter( + "waf.web.secure_server", + Parameter.OPTIONAL, null); + /** + * The name of your website, for use in page footers for example. It's not + * necessarily the URL but rather a title, e.g. "House of HTML". If not + * specified set to the server's URL. + */ + private final Parameter m_site = new StringParameter("waf.web.site_name", + Parameter.OPTIONAL, + null) { + + @Override + public final Object getDefaultValue() { + final HttpHost host = getServer(); + if (host == null) { + return null; + } else { + return host.toString(); + } + } + + }; + /** + * Sets the name and port of the machine on which the CCM instance is + * running. Used to fetch some resources by a local URL avoiding external + * internet traffic (and delay). If not specified set to the servers's name + * redirecting all traffic to external internet address. + */ + private final Parameter m_host = new HttpHostParameter("waf.web.host", + Parameter.OPTIONAL, + null) { + + @Override + public final Object getDefaultValue() { + return getServer(); + } + + }; + + /** + * List of URLs which accessed by insecure (normal HTTP) connection produce + * a redirect to a HTTPS equivalent. List is comma separated. + */ + private final Parameter m_secureRequired = new StringArrayParameter( + "waf.web.secure_required", Parameter.OPTIONAL, null); + /** + * List of URLs which accessed by secure (HTTPS) connection produce a + * redirect to a HTTP equivalent. List is comma separated. + */ + private final Parameter m_secureSwitchBack = new StringArrayParameter( + "waf.web.secure_switchback", Parameter.OPTIONAL, null); + + /** + * Dispatcher servlet path. It's the prefix to the main entry point for any + * application request (CCMDispatcherServlet). By default /ccm + */ + private final Parameter m_servlet = new StringParameter( + "waf.web.dispatcher_servlet_path", Parameter.REQUIRED, "/ccm"); + + /** + * Specifies by name which implementation of ApplicationFileResolver is used + * to dynamically resolve static files. By default + * DefaultApplicationFileResolver() is used. + */ + private final Parameter m_resolver = new SingletonParameter( + "waf.web.application_file_resolver", + Parameter.OPTIONAL, + new DefaultApplicationFileResolver()); + + private final Parameter m_deactivate_cache_host_notifications + = new BooleanParameter( + "waf.web.deactivate_cache_host_notifications", + Parameter.OPTIONAL, Boolean.FALSE); + + private final Parameter m_dynamic_host_provider = new StringParameter( + "waf.web.dynamic_host_provider", + Parameter.OPTIONAL, ""); + + /** + * Constructor, but do NOT instantiate this class directly, use + * getInstanceOf() instead. (Singleton pattern!) + * + */ + public WebConfig() { + + register(m_scheme); + register(m_server); + register(m_secureServer); + register(m_site); + register(m_host); + register(m_secureRequired); + register(m_secureSwitchBack); + register(m_servlet); + register(m_resolver); + register(m_deactivate_cache_host_notifications); + register(m_dynamic_host_provider); + + loadInfo(); + } + + public final String getDefaultScheme() { + return (String) get(m_scheme); + } + + public final HttpHost getServer() { + return (HttpHost) get(m_server); + } + + public final HttpHost getSecureServer() { + return (HttpHost) get(m_secureServer); + } + + public final boolean isSecureRequired(String uri) { + String[] secured = (String[]) get(m_secureRequired); + if (secured != null) { + for (int i = 0, n = secured.length; i < n; i++) { + if (uri.startsWith(secured[i])) { + return true; + } + } + } + return false; + } + + public final boolean isNonSecureSwitchRequired(String uri) { + String[] switchBack = (String[]) get(m_secureSwitchBack); + if (switchBack != null) { + for (int i = 0, n = switchBack.length; i < n; i++) { + if (uri.startsWith(switchBack[i])) { + return true; + } + } + } + return false; + } + + public final String getDispatcherServletPath() { + return (String) get(m_servlet); + } + + public final ApplicationFileResolver getApplicationFileResolver() { + return (ApplicationFileResolver) get(m_resolver); + } + + public final HttpHost getHost() { + return (HttpHost) get(m_host); + } + + final void setHost(final HttpHost host) { + set(m_host, host); + } + + public final String getSiteName() { + return (String) get(m_site); + } + + /** + * + * @return + * @deprecated use Web.getContextPath() instead. The installation context + * must no longer manually configured + */ + // NO LONGER configured by configuration option but determined at runtime + // by CCMDispatcherServlet itself. + // // dispatcherContextPath option in old Initializer, set to "" + // m_context = new StringParameter + // ("waf.web.dispatcher_context_path", Parameter.REQUIRED, ""); + public final String getDispatcherContextPath() { + // return (String) get(m_context); + return CCMDispatcherServlet.getContextPath(); + } + + + private static class DispatcherServletPathParameter + extends StringParameter { + + DispatcherServletPathParameter(final String name) { + super(name); + } + + @Override + protected void doValidate(final Object value, final ErrorList errors) { + final String string = (String) value; + + if (string.endsWith("/")) { + final ParameterError error = new ParameterError(this, + "The value must not end in a '/'"); + errors.add(error); + } + } + + } + + private static class DefaultSchemeParameter extends EnumerationParameter { + + DefaultSchemeParameter(final String name, + final int multiplicity, + final Object defaalt) { + super(name, multiplicity, defaalt); + + put("http", "http"); + put("https", "https"); + } + + } + + protected DynamicHostProvider dhProvider = null; + protected boolean dhProviderInited = false; + + public final DynamicHostProvider getDynamicHostProvider() { + if (dhProviderInited == false) { + String classname = (String) get(m_dynamic_host_provider); + if (classname != null) { + try { + Class klass = Class.forName(classname); + dhProvider = (DynamicHostProvider) klass.newInstance(); + } catch (Exception e) { + LOGGER.error( + "Could not instantiate DynamicHostProvider using classname : " + + classname, e); + } + } + dhProviderInited = true; + } + return dhProvider; + } + + public final boolean getDeactivateCacheHostNotifications() { + return ((Boolean) get(m_deactivate_cache_host_notifications)) + .booleanValue(); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/WebContext.java b/ccm-core/src/main/java/com/arsdigita/web/WebContext.java new file mode 100644 index 000000000..c563df3e4 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/WebContext.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.web; + +// import com.arsdigita.web.Application; +import com.arsdigita.util.Assert; +import com.arsdigita.util.Record; + +import org.apache.log4j.Logger; +import org.libreccm.core.CcmSessionContext; +import org.libreccm.core.User; +import org.libreccm.web.Application; + +/** + *

+ * A session object that provides an environment in which code can execute. The + * WebContext contains all session-specific variables. One session object is + * maintained per thread.

+ * + *

+ * Accessors of this class may return null. Developers should take care to trap + * null return values in their code.

+ * + * @author Rafael Schloming + * @author Justin Ross + * @version $Id$ + */ +public final class WebContext extends Record { + + /** + * Internal logger instance to faciliate debugging. Enable logging output by + * editing /WEB-INF/conf/log4j.properties int the runtime environment and + * set com.arsdigita.web.WebContext=DEBUG by uncommenting or adding the + * line. + */ + private static final Logger s_log = Logger.getLogger(WebContext.class); + + private Application m_application = null; + private URL m_requestURL = null; + + /** + * List of properties making up a Web Context + */ + private static String[] s_fields = new String[]{ + "User", + "Application", + "RequestURL" + }; + + /** + * Constructor + */ + WebContext() { + super(WebContext.class, s_log, s_fields); + } + + /** + * Creates a copy of this WebContext + * + * @return a new WebContext as a copy of this one + */ + final WebContext copy() { + WebContext result = new WebContext(); + + result.m_application = m_application; + result.m_requestURL = m_requestURL; + + return result; + } + + /** + * Initializes this WebContext object and setting its properties. + * + * @param app + * @param requestURL + */ + final void init(final Application app, final URL requestURL) { + setApplication(app); + setRequestURL(requestURL); + } + + final void clear() { + m_application = null; + m_requestURL = null; + } + + public final User getUser() { + CcmSessionContext context = Web.getUserContext(); + + if (context == null || !context.isLoggedIn()) { + return null; + } else { + return (User) context.getCurrentSubject(); + } + } + + /** + * + * @return + */ + public final Application getApplication() { + return m_application; + } + + /** + * + * @param app + */ + final void setApplication(final Application app) { + m_application = app; + + mutated("Application"); + } + + /** + * + * @return + */ + public final URL getRequestURL() { + return m_requestURL; + } + + /** + * + * @param url + */ + final void setRequestURL(final URL url) { + Assert.exists(url, "URL url"); + + m_requestURL = url; + + mutated("RequestURL"); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/core/CcmSessionContext.java b/ccm-core/src/main/java/org/libreccm/core/CcmSessionContext.java index d4417bd44..f05d63764 100644 --- a/ccm-core/src/main/java/org/libreccm/core/CcmSessionContext.java +++ b/ccm-core/src/main/java/org/libreccm/core/CcmSessionContext.java @@ -53,6 +53,10 @@ public class CcmSessionContext implements Serializable { this.effectiveSubject = effectiveSubject; } + public boolean isLoggedIn() { + return currentSubject != null; + } + /** * Execute code under different privileges. Useful if no current user is * available, for example in the startup phase. diff --git a/ccm-core/src/main/java/org/libreccm/core/User.java b/ccm-core/src/main/java/org/libreccm/core/User.java index 59a94ff14..4a2a4267b 100644 --- a/ccm-core/src/main/java/org/libreccm/core/User.java +++ b/ccm-core/src/main/java/org/libreccm/core/User.java @@ -183,18 +183,6 @@ public class User extends Subject implements Serializable { @XmlElement(name = "group-membership", namespace = CORE_XML_NS) private List groupMemberships; - /** - * The {@link Resource}s created by the {@code User}. - */ - @OneToMany(mappedBy = "creationUser") - private List createdResources; - - /** - * The {@link Resource}s modified by the {@code User}. - */ - @OneToMany(mappedBy = "lastModifiedUser") - private List modifiedResources; - public User() { super(); @@ -322,22 +310,6 @@ public class User extends Subject implements Serializable { groupMemberships.remove(groupMembership); } - public List getCreatedResources() { - return createdResources; - } - - public void setCreatedResources(List createdResources) { - this.createdResources = createdResources; - } - - public List getModifiedResources() { - return modifiedResources; - } - - public void setModifiedResources(List modifiedResources) { - this.modifiedResources = modifiedResources; - } - @Override public int hashCode() { int hash = super.hashCode(); diff --git a/ccm-core/src/main/java/org/libreccm/web/Application.java b/ccm-core/src/main/java/org/libreccm/web/Application.java index 09006b214..45678e7e3 100644 --- a/ccm-core/src/main/java/org/libreccm/web/Application.java +++ b/ccm-core/src/main/java/org/libreccm/web/Application.java @@ -41,6 +41,8 @@ import javax.persistence.Column; import javax.persistence.Convert; import javax.persistence.Entity; import javax.persistence.JoinColumn; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.Table; @@ -54,6 +56,10 @@ import javax.xml.bind.annotation.XmlRootElement; */ @Entity @Table(name = "APPLICATIONS", schema = DB_SCHEMA) +@NamedQueries({ + @NamedQuery(name = "retrieveApplicationForPath", + query = "SELECT a FROM Application a WHERE a.primaryUrl = :path") +}) @XmlRootElement(name = "application", namespace = WEB_XML_NS) public class Application extends Resource implements Serializable { diff --git a/ccm-core/src/main/java/org/libreccm/web/ApplicationRepository.java b/ccm-core/src/main/java/org/libreccm/web/ApplicationRepository.java new file mode 100644 index 000000000..2979e25c9 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/web/ApplicationRepository.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.web; + +import org.libreccm.core.AbstractEntityRepository; + +import javax.enterprise.context.RequestScoped; +import javax.persistence.TypedQuery; + +/** + * + * @author Jens Pelzetter + */ +@RequestScoped +public class ApplicationRepository + extends AbstractEntityRepository { + + @Override + public Class getEntityClass() { + return Application.class; + } + + @Override + public boolean isNew(final Application application) { + return application.getObjectId() == 0; + } + + public Application retrieveApplicationForPath(final String path) { + final TypedQuery query = getEntityManager() + .createNamedQuery( + "retrieveApplicationForPath", Application.class); + + return query.getSingleResult(); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/web/ServletPath.java b/ccm-core/src/main/java/org/libreccm/web/ServletPath.java new file mode 100644 index 000000000..4ec333661 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/web/ServletPath.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.web; + +import com.arsdigita.web.URL; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.servlet.annotation.WebServlet; + +/** + * Provides the path name of the location of the applications servlet/JSP. + * + * Replaces the old getServletPath method of the application class. + * Applications which have their own Servlet should be annotated with this + * annotation. The name provided here must be mapped to the Servlet by the + * {@link WebServlet} annotation or by the web.xml. + * + * NOTE: According to Servlet API the path always starts with a leading '/' and + * includes either the servlet name or a path to the servlet, but does not + * include any extra path information or a query string. Returns an empty string + * ("") is the servlet used was matched using the "/*" pattern. + * + * + * @author Jens Pelzetter + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface ServletPath { + + String value() default URL.SERVLET_DIR + "/legacy-adapter"; + +} diff --git a/ccm-docrepo/src/main/java/org/libreccm/docrepo/Resource.java b/ccm-docrepo/src/main/java/org/libreccm/docrepo/Resource.java index 19dd8fd30..a23957f96 100644 --- a/ccm-docrepo/src/main/java/org/libreccm/docrepo/Resource.java +++ b/ccm-docrepo/src/main/java/org/libreccm/docrepo/Resource.java @@ -46,13 +46,13 @@ import java.util.List; * * @author Tobias Osmers */ -@Entity +@Entity(name = "DocRepoResource") @Table(schema = "CCM_DOCREPO", name = "RESOURCES") @NamedQueries({ @NamedQuery(name = "findChildrenByParent", - query = "SELECT r FROM Resource r WHERE r.parent = :parentID"), + query = "SELECT r FROM DocRepoResource r WHERE r.parent = :parentID"), @NamedQuery(name = "findResourceByPath", - query = "SELECT r FROM Resource r WHERE r.path = :pathName")}) + query = "SELECT r FROM DocRepoResource r WHERE r.path = :pathName")}) public abstract class Resource extends CcmObject { private static final long serialVersionUID = -910317798106611214L; diff --git a/pom.xml b/pom.xml index f44bd004f..bb00d40de 100644 --- a/pom.xml +++ b/pom.xml @@ -397,7 +397,13 @@ maven-artifact 3.3.3
- + + + net.sf.jtidy + jtidy + r938 + + oro oro