From e6469906db27dcba15f69e0030584a54ef10ed91 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 15 Nov 2011 21:48:41 -0600 Subject: [PATCH 01/20] let Page objects choose to return a headless-type Reply object for custom status codes and the other goodies of Reply --- .../sitebricks/example/PageWithReply.java | 13 ++++++++ .../sitebricks/example/SitebricksConfig.java | 1 + .../src/main/resources/PageWithReply.html | 5 ++++ .../PageWithReplyAcceptanceTest.java | 23 ++++++++++++++ .../routing/WidgetRoutingDispatcher.java | 30 +++++++++++-------- 5 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PageWithReply.java create mode 100644 sitebricks-acceptance-tests/src/main/resources/PageWithReply.html create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PageWithReplyAcceptanceTest.java diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PageWithReply.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PageWithReply.java new file mode 100644 index 00000000..4ff0830b --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PageWithReply.java @@ -0,0 +1,13 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.http.Get; + +public class PageWithReply { + + @Get + public Object get() { + return Reply.saying().status(678); + + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java index f86a0b62..fa6242be 100644 --- a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java @@ -79,6 +79,7 @@ private void bindExplicitly() { at("/repeat").show(Repeat.class); at("/showif").show(ShowIf.class); at("/dynamic.js").show(DynamicJs.class); + at("/pageWithReply").show(PageWithReply.class); at("/conversion").show(Conversion.class); diff --git a/sitebricks-acceptance-tests/src/main/resources/PageWithReply.html b/sitebricks-acceptance-tests/src/main/resources/PageWithReply.html new file mode 100644 index 00000000..3ab84e84 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/PageWithReply.html @@ -0,0 +1,5 @@ + + +

for this test, the template will never be displayed because the page get method will always return a non-200 status code Reply

+ + \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PageWithReplyAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PageWithReplyAcceptanceTest.java new file mode 100644 index 00000000..ca807f51 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PageWithReplyAcceptanceTest.java @@ -0,0 +1,23 @@ +package com.google.sitebricks.acceptance; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.testng.annotations.Test; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; + +@Test(suiteName = AcceptanceTest.SUITE) +public class PageWithReplyAcceptanceTest { + + public void shouldReturnCustomStatusCode() throws IOException { + URL url = new URL(AcceptanceTest.BASE_URL + "/pageWithReply"); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + int expected = 678; + int actual = connection.getResponseCode(); + + assert actual == expected : "expected custom response code '" + expected + "' but was '" + actual + "'"; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java index 9a19f9cc..1ed1b4e8 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java @@ -1,5 +1,9 @@ package com.google.sitebricks.routing; +import java.io.IOException; + +import net.jcip.annotations.Immutable; + import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -8,12 +12,10 @@ import com.google.sitebricks.binding.FlashCache; import com.google.sitebricks.binding.RequestBinder; import com.google.sitebricks.headless.HeadlessRenderer; +import com.google.sitebricks.headless.Reply; import com.google.sitebricks.headless.Request; import com.google.sitebricks.rendering.resource.ResourcesService; import com.google.sitebricks.routing.PageBook.Page; -import net.jcip.annotations.Immutable; - -import java.io.IOException; /** * @author Dhanji R. Prasanna (dhanji@gmail.com) @@ -69,14 +71,12 @@ public Object dispatch(Request request, Events event) throws IOException { final Object instance = page.instantiate(); if (page.isHeadless()) { return bindAndReply(request, page, instance); - } else { - respond = new StringBuilderRespond(instance); - - //fire events and render reponders - bindAndRespond(request, page, respond, instance); } + + //fire events and render reponders + return bindAndRespond(request, page, instance); + - return respond; } private Object bindAndReply(Request request, Page page, Object instance) throws IOException { @@ -87,8 +87,9 @@ private Object bindAndReply(Request request, Page page, Object instance) throws return fireEvent(request, page, instance); } - private void bindAndRespond(Request request, PageBook.Page page, Respond respond, - Object instance) { + private Object bindAndRespond(Request request, PageBook.Page page, + Object instance) throws IOException { + Respond respond = new StringBuilderRespond(instance); //bind request binder.bind(request, instance); @@ -105,6 +106,9 @@ else if (redirect instanceof Class) { // should never be null coz it is validated on compile. respond.redirect(contextualize(request, targetPage.getUri())); + } else if (redirect instanceof Reply) { + //page wants to be headless + return bindAndReply(request, page, instance); } else { // Handle page-chaining driven redirection. PageBook.Page targetPage = book.forInstance(redirect); @@ -116,8 +120,10 @@ else if (redirect instanceof Class) { // verified at compile, not be a variablized matcher. respond.redirect(contextualize(request, targetPage.getUri())); } - } else + } else { page.widget().render(instance, respond); + } + return respond; } // We're sure the request parameter map is a Map From 978ecab335b202bf5f4c68a3e261e3f77c5afb45 Mon Sep 17 00:00:00 2001 From: james Date: Mon, 12 Dec 2011 09:40:06 -0600 Subject: [PATCH 02/20] sitebricks 0.8.5 tar.gz from github --- sitebricks-acceptance-tests/pom.xml | 137 +++ .../com/google/sitebricks/example/Case.java | 19 + .../sitebricks/example/CompileErrors.java | 21 + .../example/ContentNegotiation.java | 29 + .../google/sitebricks/example/Conversion.java | 48 ++ .../sitebricks/example/DecoratedPage.java | 16 + .../sitebricks/example/DecoratorPage.java | 12 + .../google/sitebricks/example/DynamicJs.java | 17 + .../com/google/sitebricks/example/Embed.java | 23 + .../com/google/sitebricks/example/Forms.java | 40 + .../google/sitebricks/example/HelloWorld.java | 27 + .../sitebricks/example/HelloWorldService.java | 48 ++ .../sitebricks/example/HiddenFieldMethod.java | 43 + .../com/google/sitebricks/example/I18n.java | 67 ++ .../example/MvelTemplateExample.java | 17 + .../google/sitebricks/example/NextPage.java | 21 + .../google/sitebricks/example/PageChain.java | 26 + .../example/PostableRestfulWebService.java | 40 + .../com/google/sitebricks/example/Repeat.java | 31 + .../sitebricks/example/RestfulWebService.java | 83 ++ .../RestfulWebServiceNoAnnotations.java | 40 + .../example/RestfulWebServiceWithCRUD.java | 61 ++ .../RestfulWebServiceWithCRUDConversions.java | 196 +++++ .../RestfulWebServiceWithMatrixParams.java | 31 + .../RestfulWebServiceWithSubpaths.java | 56 ++ .../RestfulWebServiceWithSubpaths2.java | 98 +++ .../sitebricks/example/SelectRouting.java | 118 +++ .../com/google/sitebricks/example/ShowIf.java | 24 + .../sitebricks/example/SitebricksConfig.java | 145 ++++ .../com/google/sitebricks/example/Start.java | 31 + .../google/sitebricks/example/StartAware.java | 17 + .../google/sitebricks/example/TestPage.java | 28 + .../src/main/resources/Case.html | 71 ++ .../src/main/resources/CompileErrors.html | 61 ++ .../main/resources/ContentNegotiation.html | 8 + .../src/main/resources/Conversion.html | 73 ++ .../src/main/resources/DecoratedPage.html | 7 + .../src/main/resources/Decorator.html | 19 + .../src/main/resources/Embed.html | 48 ++ .../src/main/resources/Forms.html | 88 ++ .../src/main/resources/HelloWorld.html | 67 ++ .../src/main/resources/HiddenFieldMethod.html | 83 ++ .../src/main/resources/I18n.html | 68 ++ .../main/resources/MvelTemplateExample.mvel | 52 ++ .../src/main/resources/NextPage.html | 52 ++ .../src/main/resources/PageChain.html | 64 ++ .../src/main/resources/Repeat.html | 73 ++ .../src/main/resources/SelectRouting.html | 228 +++++ .../src/main/resources/ShowIf.html | 62 ++ .../src/main/resources/TestPage.html | 8 + .../src/main/resources/WEB-INF/web.xml | 22 + .../src/main/resources/default.css | 339 ++++++++ .../src/main/resources/dynamic.js | 3 + .../src/main/resources/index.html | 69 ++ .../ServletContainerIntegrationTest.java | 48 ++ .../acceptance/CaseAcceptanceTest.java | 19 + .../acceptance/ConnegAcceptanceTest.java | 41 + .../acceptance/ConversionAcceptanceTest.java | 31 + .../acceptance/DecoratorAcceptanceTest.java | 25 + .../acceptance/DynamicJsAcceptanceTest.java | 20 + .../acceptance/EmbedAcceptanceTest.java | 21 + .../acceptance/FormsAcceptanceTest.java | 31 + .../acceptance/HelloWorldAcceptanceTest.java | 41 + .../HiddenFieldMethodAcceptanceTest.java | 25 + .../acceptance/I18nAcceptanceTest.java | 25 + .../acceptance/JettyAcceptanceTest.java | 40 + .../PageChainingAcceptanceTest.java | 31 + ...ostableRestfuWebServiceAcceptanceTest.java | 49 ++ .../acceptance/RepeatAcceptanceTest.java | 40 + .../RestfuWebServiceAcceptanceTest.java | 99 +++ ...estfuWebServiceWithCRUDAcceptanceTest.java | 104 +++ ...viceWithCRUDConversionsAcceptanceTest.java | 111 +++ ...ServiceWithMatrixParamsAcceptanceTest.java | 57 ++ ...WebServiceWithSubpaths2AcceptanceTest.java | 167 ++++ ...uWebServiceWithSubpathsAcceptanceTest.java | 112 +++ .../SelectRoutingAcceptanceTest.java | 254 ++++++ ...WebServiceWithSubpaths2AcceptanceTest.java | 52 ++ .../acceptance/StatsAcceptanceTest.java | 24 + .../sitebricks/acceptance/page/CasePage.java | 29 + .../acceptance/page/ConnegPage.java | 45 + .../acceptance/page/ConversionPage.java | 95 +++ .../acceptance/page/DecoratorPage.java | 36 + .../acceptance/page/DynamicJsPage.java | 24 + .../sitebricks/acceptance/page/EmbedPage.java | 32 + .../sitebricks/acceptance/page/FormsPage.java | 54 ++ .../acceptance/page/HelloWorldPage.java | 38 + .../page/HiddenFieldMethodPage.java | 42 + .../sitebricks/acceptance/page/I18nPage.java | 45 + .../acceptance/page/PageChainPage.java | 31 + .../acceptance/page/RepeatPage.java | 49 ++ .../acceptance/page/SelectRoutingPage.java | 44 + .../sitebricks/acceptance/page/StatsPage.java | 32 + .../acceptance/util/AcceptanceTest.java | 16 + .../sitebricks/acceptance/util/Jetty.java | 45 + sitebricks-client/pom.xml | 60 ++ .../sitebricks/client/AHCWebClient.java | 152 ++++ .../google/sitebricks/client/CommonsWeb.java | 28 + .../google/sitebricks/client/Transport.java | 48 ++ .../sitebricks/client/TransportException.java | 10 + .../com/google/sitebricks/client/Web.java | 29 + .../google/sitebricks/client/WebClient.java | 19 + .../sitebricks/client/WebClientBuilder.java | 72 ++ .../google/sitebricks/client/WebResponse.java | 20 + .../sitebricks/client/WebResponseImpl.java | 89 ++ .../client/transport/ByteArrayTransport.java | 24 + .../transport/JacksonJsonTransport.java | 169 ++++ .../sitebricks/client/transport/Json.java | 18 + .../sitebricks/client/transport/Raw.java | 18 + .../client/transport/SimpleTextTransport.java | 24 + .../sitebricks/client/transport/Text.java | 18 + .../client/transport/XStreamXmlTransport.java | 28 + .../sitebricks/client/transport/Xml.java | 18 + .../client/WebClientEdslIntegrationTest.java | 43 + .../client/WebClientIntegrationTest.java | 24 + .../client/transport/RawTransportTest.java | 39 + .../transport/SimpleTextTransportTest.java | 37 + .../client/transport/XmlTransportTest.java | 78 ++ .../com/google/sitebricks/client/tweet.json | 19 + sitebricks-converter/pom.xml | 44 + .../sitebricks/conversion/Converter.java | 18 + .../conversion/ConverterAdaptor.java | 11 + .../conversion/ConverterRegistry.java | 15 + .../sitebricks/conversion/ConverterUtils.java | 34 + .../sitebricks/conversion/DateConverters.java | 170 ++++ .../conversion/DummyTypeConverter.java | 19 + .../conversion/MvelConversionHandlers.java | 69 ++ .../conversion/MvelTypeConverter.java | 54 ++ .../conversion/NumberConverters.java | 53 ++ .../conversion/ObjectToStringConverter.java | 12 + .../conversion/SingletonListConverter.java | 17 + .../conversion/StandardTypeConverter.java | 219 +++++ .../StringToPrimitiveConverters.java | 50 ++ .../sitebricks/conversion/TypeConverter.java | 23 + .../conversion/generics/CaptureType.java | 35 + .../conversion/generics/CaptureTypeImpl.java | 71 ++ .../generics/GenericArrayTypeImpl.java | 64 ++ .../conversion/generics/Generics.java | 547 ++++++++++++ .../generics/ParameterizedTypeImpl.java | 95 +++ .../conversion/generics/TypeToken.java | 97 +++ .../conversion/generics/VarMap.java | 94 +++ .../conversion/generics/WildcardTypeImpl.java | 67 ++ .../conversion/MvelTypeConverterTest.java | 14 + .../conversion/StandardTypeConverterTest.java | 97 +++ .../conversion/TestTypeConverter.java | 15 + sitebricks-jetty-archetype/pom.xml | 39 + .../info/sitebricks/example/AppConfig.java | 29 + .../java/info/sitebricks/example/Main.java | 25 + .../info/sitebricks/example/web/HomePage.java | 32 + .../src/main/resources/HomePage.html | 14 + .../src/main/resources/WEB-INF/web.xml | 30 + sitebricks-mail/pom.xml | 81 ++ .../sitebricks/mail/CommandCompletion.java | 51 ++ .../sitebricks/mail/FolderObserver.java | 21 + .../java/com/google/sitebricks/mail/Mail.java | 24 + .../google/sitebricks/mail/MailClient.java | 87 ++ .../sitebricks/mail/MailClientConfig.java | 49 ++ .../sitebricks/mail/MailClientHandler.java | 109 +++ .../mail/MailClientPipelineFactory.java | 50 ++ .../sitebricks/mail/NettyImapClient.java | 205 +++++ .../sitebricks/mail/SitebricksMail.java | 65 ++ .../java/com/google/sitebricks/mail/example | 20 + .../google/sitebricks/mail/imap/Command.java | 43 + .../sitebricks/mail/imap/Extractor.java | 12 + .../com/google/sitebricks/mail/imap/Flag.java | 18 + .../google/sitebricks/mail/imap/Folder.java | 27 + .../sitebricks/mail/imap/FolderExtractor.java | 36 + .../sitebricks/mail/imap/FolderStatus.java | 55 ++ .../mail/imap/FolderStatusExtractor.java | 42 + .../mail/imap/ListFoldersExtractor.java | 40 + .../google/sitebricks/mail/imap/Message.java | 11 + .../mail/imap/MessageExtractor.java | 69 ++ .../sitebricks/mail/imap/MessageStatus.java | 71 ++ .../mail/imap/MessageStatusExtractor.java | 202 +++++ .../mail/MailClientIntegrationTest.java | 62 ++ .../mail/imap/MessageStatusExtractorTest.java | 45 + .../mail/test/integration/TestImap.java | 24 + .../google/sitebricks/mail/webapp/Home.java | 13 + .../sitebricks/mail/webapp/WebConfig.java | 21 + sitebricks-mail/src/test/resources/Home.html | 10 + .../src/test/resources/WEB-INF/web.xml | 22 + .../sitebricks/mail/imap/fetch_all_data.txt | 9 + sitebricks-options/pom.xml | 66 ++ .../google/sitebricks/options/Options.java | 18 + .../sitebricks/options/OptionsModule.java | 192 +++++ .../sitebricks/options/OptionsTest.java | 141 ++++ .../sitebricks/options/options.properties | 3 + sitebricks-web/pom.xml | 69 ++ .../src/main/java/info/sitebricks/Jetty.java | 45 + .../info/sitebricks/SitebricksConfig.java | 32 + .../java/info/sitebricks/data/Document.java | 79 ++ .../main/java/info/sitebricks/data/Index.java | 53 ++ .../info/sitebricks/persist/PersistAware.java | 31 + .../sitebricks/persist/PersistFilter.java | 46 ++ .../info/sitebricks/persist/StoreModule.java | 17 + .../info/sitebricks/persist/WikiStore.java | 67 ++ .../main/java/info/sitebricks/web/Home.java | 50 ++ .../java/info/sitebricks/web/WikiService.java | 50 ++ sitebricks-web/src/main/resources/Home.html | 52 ++ .../src/main/resources/WEB-INF/web.xml | 21 + .../info/sitebricks/data/Document.html | 15 + .../src/main/resources/js/jquery-1.4.3.min.js | 166 ++++ sitebricks-web/src/main/resources/js/json2.js | 324 ++++++++ sitebricks-web/src/main/resources/js/rpc.js | 41 + .../src/main/resources/js/sitebricks.js | 46 ++ sitebricks-web/src/main/resources/main.css | 255 ++++++ sitebricks/TODO | 18 + sitebricks/pom.atom | 23 + sitebricks/pom.xml | 140 ++++ .../google/sitebricks/ActionDescriptor.java | 94 +++ .../main/java/com/google/sitebricks/At.java | 15 + .../java/com/google/sitebricks/Aware.java | 10 + .../com/google/sitebricks/AwareModule.java | 26 + .../com/google/sitebricks/Bootstrapper.java | 16 + .../java/com/google/sitebricks/Bricks.java | 14 + .../java/com/google/sitebricks/Classes.java | 186 +++++ .../google/sitebricks/DebugModePageBook.java | 125 +++ .../DebugModeRoutingDispatcher.java | 125 +++ .../java/com/google/sitebricks/Evaluator.java | 17 + .../java/com/google/sitebricks/Export.java | 25 + .../java/com/google/sitebricks/GaeModule.java | 23 + .../google/sitebricks/HiddenMethodFilter.java | 101 +++ .../java/com/google/sitebricks/Localizer.java | 262 ++++++ .../sitebricks/MissingTemplateException.java | 10 + .../com/google/sitebricks/MvelEvaluator.java | 70 ++ .../sitebricks/NoSuchResourceException.java | 14 + .../PackageScanFailedException.java | 11 + .../com/google/sitebricks/PageBinder.java | 61 ++ .../com/google/sitebricks/Renderable.java | 18 + .../java/com/google/sitebricks/Respond.java | 45 + .../ScanAndCompileBootstrapper.java | 283 +++++++ .../main/java/com/google/sitebricks/Show.java | 16 + .../com/google/sitebricks/Shutdowner.java | 27 + .../google/sitebricks/SitebricksFilter.java | 83 ++ .../sitebricks/SitebricksInternalModule.java | 207 +++++ .../google/sitebricks/SitebricksModule.java | 322 ++++++++ .../sitebricks/SitebricksServletModule.java | 87 ++ .../sitebricks/StringBuilderRespond.java | 139 ++++ .../java/com/google/sitebricks/Template.java | 44 + .../com/google/sitebricks/TemplateLoader.java | 176 ++++ .../sitebricks/TemplateLoadingException.java | 14 + .../java/com/google/sitebricks/Visible.java | 24 + .../binding/ConcurrentPropertyCache.java | 44 + .../binding/CookieBasedFlashCache.java | 97 +++ .../google/sitebricks/binding/FlashCache.java | 18 + .../sitebricks/binding/GaeFlashCache.java | 42 + .../binding/HttpSessionFlashCache.java | 32 + .../binding/InvalidBindingException.java | 10 + .../sitebricks/binding/MvelRequestBinder.java | 123 +++ .../sitebricks/binding/NoFlashCache.java | 23 + .../sitebricks/binding/PropertyCache.java | 11 + .../sitebricks/binding/RequestBinder.java | 15 + .../sitebricks/compiler/AnalysisError.java | 104 +++ .../sitebricks/compiler/AnalysisErrors.java | 11 + .../sitebricks/compiler/AnnotationNode.java | 62 ++ .../sitebricks/compiler/AnnotationParser.java | 52 ++ .../sitebricks/compiler/CompileError.java | 126 +++ .../sitebricks/compiler/CompileErrors.java | 19 + .../sitebricks/compiler/CompiledToken.java | 63 ++ .../google/sitebricks/compiler/Compilers.java | 43 + .../com/google/sitebricks/compiler/Dom.java | 190 +++++ .../compiler/EvaluatorCompiler.java | 71 ++ .../compiler/ExpressionCompileException.java | 33 + .../compiler/FlatTemplateCompiler.java | 50 ++ .../sitebricks/compiler/HtmlParser.java | 562 +++++++++++++ .../compiler/HtmlTemplateCompiler.java | 673 +++++++++++++++ .../compiler/MvelEvaluatorCompiler.java | 225 +++++ .../google/sitebricks/compiler/Parsing.java | 262 ++++++ .../sitebricks/compiler/RepeatToken.java | 15 + .../compiler/RequireWidgetInternPool.java | 13 + .../compiler/StandardCompilers.java | 158 ++++ .../compiler/TemplateCompileException.java | 130 +++ .../compiler/TemplateParseException.java | 10 + .../com/google/sitebricks/compiler/Token.java | 28 + .../compiler/XmlTemplateCompiler.java | 505 ++++++++++++ .../template/MvelTemplateCompiler.java | 39 + .../FreemarkerTemplateCompiler.java | 78 ++ .../google/sitebricks/core/CaseWidget.java | 20 + .../com/google/sitebricks/core/Repeat.java | 76 ++ .../com/google/sitebricks/core/ShowIf.java | 39 + .../google/sitebricks/core/package-info.java | 1 + .../google/sitebricks/debug/DebugPage.html | 33 + .../google/sitebricks/debug/DebugPage.java | 53 ++ .../sitebricks/headless/HeadlessRenderer.java | 16 + .../com/google/sitebricks/headless/Reply.java | 136 +++ .../headless/ReplyBasedHeadlessRenderer.java | 34 + .../sitebricks/headless/ReplyMaker.java | 196 +++++ .../google/sitebricks/headless/Request.java | 79 ++ .../google/sitebricks/headless/Service.java | 49 ++ .../com/google/sitebricks/http/Delete.java | 16 + .../java/com/google/sitebricks/http/Get.java | 15 + .../java/com/google/sitebricks/http/Head.java | 15 + .../java/com/google/sitebricks/http/Post.java | 16 + .../java/com/google/sitebricks/http/Put.java | 16 + .../com/google/sitebricks/http/Select.java | 34 + .../com/google/sitebricks/http/Trace.java | 15 + .../sitebricks/http/negotiate/Accept.java | 40 + .../http/negotiate/ConnegModule.java | 15 + .../http/negotiate/ContentNegotiator.java | 31 + .../http/negotiate/ExactMatchNegotiator.java | 37 + .../http/negotiate/Negotiation.java | 16 + .../http/negotiate/RegexNegotiator.java | 38 + .../http/negotiate/WildcardNegotiator.java | 100 +++ .../com/google/sitebricks/i18n/Message.java | 14 + .../sitebricks/rendering/Attributes.java | 17 + .../sitebricks/rendering/Decorated.java | 18 + .../google/sitebricks/rendering/EmbedAs.java | 16 + .../sitebricks/rendering/SelfRendering.java | 14 + .../google/sitebricks/rendering/Strings.java | 43 + .../sitebricks/rendering/Templates.java | 85 ++ .../com/google/sitebricks/rendering/With.java | 16 + .../rendering/control/ArgumentWidget.java | 45 + .../sitebricks/rendering/control/Chains.java | 23 + .../rendering/control/ChooseWidget.java | 75 ++ .../rendering/control/DecorateWidget.java | 101 +++ .../control/DefaultWidgetRegistry.java | 164 ++++ .../rendering/control/EmbedWidget.java | 78 ++ .../rendering/control/EmbeddedRespond.java | 132 +++ .../control/EmbeddedRespondFactory.java | 24 + .../rendering/control/HeaderWidget.java | 46 ++ .../rendering/control/IncludeWidget.java | 30 + .../control/NoSuchWidgetException.java | 10 + .../control/ProceedingWidgetChain.java | 52 ++ .../rendering/control/RawTextWidget.java | 28 + .../rendering/control/RepeatWidget.java | 92 +++ .../rendering/control/RequireWidget.java | 42 + .../rendering/control/ShowIfWidget.java | 39 + .../control/SingletonWidgetChain.java | 31 + .../control/TerminalWidgetChain.java | 30 + .../rendering/control/TextFieldWidget.java | 36 + .../rendering/control/TextWidget.java | 43 + .../rendering/control/WidgetChain.java | 10 + .../rendering/control/WidgetRegistry.java | 45 + .../rendering/control/WidgetWrapper.java | 113 +++ .../rendering/control/XmlDirectiveWidget.java | 39 + .../rendering/control/XmlWidget.java | 137 +++ .../sitebricks/rendering/resource/Assets.java | 27 + .../resource/ClasspathResourcesService.java | 179 ++++ .../resource/ResourceLoadingException.java | 14 + .../rendering/resource/ResourcesService.java | 15 + .../com/google/sitebricks/routing/Action.java | 31 + .../sitebricks/routing/DefaultPageBook.java | 778 ++++++++++++++++++ .../routing/EventDispatchException.java | 10 + .../routing/InMemorySystemMetrics.java | 103 +++ .../routing/InvalidEventHandlerException.java | 10 + .../google/sitebricks/routing/PageBook.java | 142 ++++ .../sitebricks/routing/PathMatcher.java | 14 + .../sitebricks/routing/PathMatcherChain.java | 141 ++++ .../google/sitebricks/routing/Production.java | 13 + .../sitebricks/routing/RoutingDispatcher.java | 16 + .../sitebricks/routing/ServiceAction.java | 32 + .../google/sitebricks/routing/SpiAction.java | 56 ++ .../sitebricks/routing/SystemMetrics.java | 39 + .../routing/WidgetRoutingDispatcher.java | 143 ++++ .../google/sitebricks/core/CaseWidget.html | 6 + .../com/google/sitebricks/core/ll.gif | Bin 0 -> 46 bytes .../com/google/sitebricks/core/lr.gif | Bin 0 -> 47 bytes .../com/google/sitebricks/core/ul.gif | Bin 0 -> 46 bytes .../com/google/sitebricks/core/ur.gif | Bin 0 -> 47 bytes .../rendering/resource/mimetypes.properties | 9 + .../google/sitebricks/templates.properties | 2 + .../java/com/google/sitebricks/EdslTest.java | 70 ++ .../google/sitebricks/LocalizationTest.java | 289 +++++++ .../com/google/sitebricks/RespondTest.java | 30 + .../sitebricks/RespondersForTesting.java | 13 + .../google/sitebricks/TemplateLoaderTest.java | 83 ++ .../google/sitebricks/WidgetFilterTest.java | 156 ++++ .../binding/MvelRequestBinderTest.java | 214 +++++ .../sitebricks/binding/PropertyCacheTest.java | 21 + .../FreemarkerTemplateCompilerTest.java | 422 ++++++++++ .../compiler/HtmlTemplateCompilerTest.java | 486 +++++++++++ .../compiler/XmlLineNumberParsingTest.java | 45 + .../compiler/XmlTemplateCompilerTest.java | 459 +++++++++++ .../headless/HeadlessReplyTest.java | 230 ++++++ .../sitebricks/headless/ReplyEdslTest.java | 59 ++ .../negotiate/ExactMatchNegotiatorTest.java | 79 ++ .../http/negotiate/RegexNegotiatorTest.java | 104 +++ .../negotiate/WildcardNegotiatorTest.java | 124 +++ .../DynTypedMvelEvaluatorCompiler.java | 71 ++ .../rendering/EvaluatorCompilerTest.java | 260 ++++++ .../rendering/MvelGenericsConfidenceTest.java | 67 ++ .../sitebricks/rendering/ParsingTest.java | 76 ++ .../rendering/control/ChooseWidgetTest.java | 61 ++ .../rendering/control/EmbedWidgetTest.java | 467 +++++++++++ .../control/EmbeddedRespondExtractorTest.java | 140 ++++ .../rendering/control/HeaderWidgetTest.java | 65 ++ .../rendering/control/RepeatWidgetTest.java | 117 +++ .../rendering/control/RequireWidgetTest.java | 51 ++ .../rendering/control/ShowIfWidgetTest.java | 46 ++ .../control/TextFieldWidgetTest.java | 37 + .../rendering/control/TextWidgetTest.java | 110 +++ .../rendering/control/WidgetRegistryTest.java | 38 + .../MimeTypesRegexIntegrationTest.java | 31 + .../resource/ResourcesServiceTest.java | 0 .../sitebricks/routing/PageBookImplTest.java | 732 ++++++++++++++++ .../sitebricks/routing/PathMatcherTest.java | 149 ++++ .../routing/WidgetRoutingDispatcherTest.java | 456 ++++++++++ .../test/ContentNegotiationExample.java | 28 + .../com/google/sitebricks/test/Search.html | 50 ++ .../com/google/sitebricks/test/Search.java | 49 ++ .../java/com/google/sitebricks/test/Wiki.html | 32 + .../java/com/google/sitebricks/test/Wiki.java | 35 + .../google/sitebricks/util/TextToolsTest.java | 56 ++ .../resources/com/google/sitebricks/My.xml | 6 + .../com/google/sitebricks/MyHtml.html | 6 + .../com/google/sitebricks/MyXhtml.xhtml | 6 + .../sitebricks/rendering/resource/my.xml | 3 + slf4j/pom.xml | 58 ++ .../slf4j/Slf4jInjectionTypeListener.java | 55 ++ .../google/sitebricks/slf4j/Slf4jModule.java | 18 + .../slf4j/Slf4jIntegrationTest.java | 55 ++ stat/pom.xml | 89 ++ .../stat/MemberAnnotatedWithAtStat.java | 69 ++ .../java/com/google/sitebricks/stat/Stat.java | 28 + .../stat/StatAnnotatedTypeListener.java | 32 + .../google/sitebricks/stat/StatCollector.java | 90 ++ .../sitebricks/stat/StatDescriptor.java | 77 ++ .../google/sitebricks/stat/StatExposer.java | 36 + .../google/sitebricks/stat/StatExposers.java | 68 ++ .../google/sitebricks/stat/StatModule.java | 195 +++++ .../google/sitebricks/stat/StatReader.java | 54 ++ .../google/sitebricks/stat/StatReaders.java | 189 +++++ .../google/sitebricks/stat/StatRegistrar.java | 97 +++ .../com/google/sitebricks/stat/Stats.java | 103 +++ .../sitebricks/stat/StatsPublisher.java | 44 + .../sitebricks/stat/StatsPublishers.java | 94 +++ .../google/sitebricks/stat/StatsServlet.java | 58 ++ .../sitebricks/stat/StatExposersTest.java | 114 +++ .../sitebricks/stat/StatReadersTest.java | 143 ++++ .../sitebricks/stat/StatsIntegrationTest.java | 234 ++++++ .../sitebricks/stat/StatsPublishersTest.java | 105 +++ .../stat/testservices/ChildDummyService.java | 45 + .../stat/testservices/DummyService.java | 63 ++ .../StatExposerTestingService.java | 98 +++ .../stat/testservices/StaticDummyService.java | 47 ++ 434 files changed, 31928 insertions(+) create mode 100644 sitebricks-acceptance-tests/pom.xml create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Case.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CompileErrors.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/ContentNegotiation.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Conversion.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DecoratedPage.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DecoratorPage.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DynamicJs.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Embed.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Forms.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HelloWorld.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HelloWorldService.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HiddenFieldMethod.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/I18n.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/MvelTemplateExample.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/NextPage.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PageChain.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PostableRestfulWebService.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Repeat.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebService.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceNoAnnotations.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithCRUD.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithCRUDConversions.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithMatrixParams.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithSubpaths.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithSubpaths2.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SelectRouting.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/ShowIf.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Start.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/StartAware.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/TestPage.java create mode 100644 sitebricks-acceptance-tests/src/main/resources/Case.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/CompileErrors.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/ContentNegotiation.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/Conversion.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/DecoratedPage.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/Decorator.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/Embed.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/Forms.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/HelloWorld.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/HiddenFieldMethod.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/I18n.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/MvelTemplateExample.mvel create mode 100644 sitebricks-acceptance-tests/src/main/resources/NextPage.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/PageChain.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/Repeat.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/SelectRouting.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/ShowIf.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/TestPage.html create mode 100644 sitebricks-acceptance-tests/src/main/resources/WEB-INF/web.xml create mode 100644 sitebricks-acceptance-tests/src/main/resources/default.css create mode 100644 sitebricks-acceptance-tests/src/main/resources/dynamic.js create mode 100644 sitebricks-acceptance-tests/src/main/resources/index.html create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/ServletContainerIntegrationTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/CaseAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/ConnegAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/ConversionAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/DecoratorAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/DynamicJsAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/EmbedAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/FormsAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/HelloWorldAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/HiddenFieldMethodAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/I18nAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/JettyAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PageChainingAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PostableRestfuWebServiceAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RepeatAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithCRUDAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithCRUDConversionsAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithMatrixParamsAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithSubpaths2AcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithSubpathsAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/SelectRoutingAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/SpiRestfuWebServiceWithSubpaths2AcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/StatsAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/CasePage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/ConnegPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/ConversionPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/DecoratorPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/DynamicJsPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/EmbedPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/FormsPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/HelloWorldPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/HiddenFieldMethodPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/I18nPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/PageChainPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/RepeatPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/SelectRoutingPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/StatsPage.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/util/AcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/util/Jetty.java create mode 100644 sitebricks-client/pom.xml create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/AHCWebClient.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/CommonsWeb.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/Transport.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/TransportException.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/Web.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/WebClient.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/WebClientBuilder.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/WebResponse.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/WebResponseImpl.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/transport/ByteArrayTransport.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/transport/JacksonJsonTransport.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Json.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Raw.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/transport/SimpleTextTransport.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Text.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/transport/XStreamXmlTransport.java create mode 100644 sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Xml.java create mode 100644 sitebricks-client/src/test/java/com/google/sitebricks/client/WebClientEdslIntegrationTest.java create mode 100644 sitebricks-client/src/test/java/com/google/sitebricks/client/WebClientIntegrationTest.java create mode 100644 sitebricks-client/src/test/java/com/google/sitebricks/client/transport/RawTransportTest.java create mode 100644 sitebricks-client/src/test/java/com/google/sitebricks/client/transport/SimpleTextTransportTest.java create mode 100644 sitebricks-client/src/test/java/com/google/sitebricks/client/transport/XmlTransportTest.java create mode 100644 sitebricks-client/src/test/resources/com/google/sitebricks/client/tweet.json create mode 100644 sitebricks-converter/pom.xml create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/Converter.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterAdaptor.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterRegistry.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterUtils.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/DateConverters.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/DummyTypeConverter.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/MvelConversionHandlers.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/MvelTypeConverter.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/NumberConverters.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ObjectToStringConverter.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/SingletonListConverter.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/StandardTypeConverter.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/StringToPrimitiveConverters.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/TypeConverter.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/CaptureType.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/CaptureTypeImpl.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/GenericArrayTypeImpl.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/Generics.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/ParameterizedTypeImpl.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/TypeToken.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/VarMap.java create mode 100644 sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/WildcardTypeImpl.java create mode 100644 sitebricks-converter/src/test/java/com/google/sitebricks/conversion/MvelTypeConverterTest.java create mode 100644 sitebricks-converter/src/test/java/com/google/sitebricks/conversion/StandardTypeConverterTest.java create mode 100644 sitebricks-converter/src/test/java/com/google/sitebricks/conversion/TestTypeConverter.java create mode 100644 sitebricks-jetty-archetype/pom.xml create mode 100644 sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/AppConfig.java create mode 100644 sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/Main.java create mode 100644 sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/web/HomePage.java create mode 100644 sitebricks-jetty-archetype/src/main/resources/HomePage.html create mode 100644 sitebricks-jetty-archetype/src/main/resources/WEB-INF/web.xml create mode 100644 sitebricks-mail/pom.xml create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/CommandCompletion.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/FolderObserver.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/Mail.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClient.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientConfig.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientHandler.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientPipelineFactory.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/NettyImapClient.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/SitebricksMail.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/example create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Command.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Extractor.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Flag.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Folder.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderExtractor.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderStatus.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderStatusExtractor.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/ListFoldersExtractor.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Message.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageExtractor.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageStatus.java create mode 100644 sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageStatusExtractor.java create mode 100644 sitebricks-mail/src/test/java/com/google/sitebricks/mail/MailClientIntegrationTest.java create mode 100644 sitebricks-mail/src/test/java/com/google/sitebricks/mail/imap/MessageStatusExtractorTest.java create mode 100644 sitebricks-mail/src/test/java/com/google/sitebricks/mail/test/integration/TestImap.java create mode 100644 sitebricks-mail/src/test/java/com/google/sitebricks/mail/webapp/Home.java create mode 100644 sitebricks-mail/src/test/java/com/google/sitebricks/mail/webapp/WebConfig.java create mode 100644 sitebricks-mail/src/test/resources/Home.html create mode 100644 sitebricks-mail/src/test/resources/WEB-INF/web.xml create mode 100644 sitebricks-mail/src/test/resources/com/google/sitebricks/mail/imap/fetch_all_data.txt create mode 100644 sitebricks-options/pom.xml create mode 100644 sitebricks-options/src/main/java/com/google/sitebricks/options/Options.java create mode 100644 sitebricks-options/src/main/java/com/google/sitebricks/options/OptionsModule.java create mode 100644 sitebricks-options/src/test/java/com/google/sitebricks/options/OptionsTest.java create mode 100644 sitebricks-options/src/test/resources/com/google/sitebricks/options/options.properties create mode 100644 sitebricks-web/pom.xml create mode 100644 sitebricks-web/src/main/java/info/sitebricks/Jetty.java create mode 100644 sitebricks-web/src/main/java/info/sitebricks/SitebricksConfig.java create mode 100644 sitebricks-web/src/main/java/info/sitebricks/data/Document.java create mode 100644 sitebricks-web/src/main/java/info/sitebricks/data/Index.java create mode 100644 sitebricks-web/src/main/java/info/sitebricks/persist/PersistAware.java create mode 100644 sitebricks-web/src/main/java/info/sitebricks/persist/PersistFilter.java create mode 100644 sitebricks-web/src/main/java/info/sitebricks/persist/StoreModule.java create mode 100644 sitebricks-web/src/main/java/info/sitebricks/persist/WikiStore.java create mode 100644 sitebricks-web/src/main/java/info/sitebricks/web/Home.java create mode 100644 sitebricks-web/src/main/java/info/sitebricks/web/WikiService.java create mode 100644 sitebricks-web/src/main/resources/Home.html create mode 100644 sitebricks-web/src/main/resources/WEB-INF/web.xml create mode 100644 sitebricks-web/src/main/resources/info/sitebricks/data/Document.html create mode 100644 sitebricks-web/src/main/resources/js/jquery-1.4.3.min.js create mode 100644 sitebricks-web/src/main/resources/js/json2.js create mode 100644 sitebricks-web/src/main/resources/js/rpc.js create mode 100644 sitebricks-web/src/main/resources/js/sitebricks.js create mode 100644 sitebricks-web/src/main/resources/main.css create mode 100644 sitebricks/TODO create mode 100644 sitebricks/pom.atom create mode 100644 sitebricks/pom.xml create mode 100644 sitebricks/src/main/java/com/google/sitebricks/ActionDescriptor.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/At.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Aware.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/AwareModule.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Bootstrapper.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Bricks.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Classes.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/DebugModePageBook.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/DebugModeRoutingDispatcher.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Evaluator.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Export.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/GaeModule.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/HiddenMethodFilter.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Localizer.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/MissingTemplateException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/MvelEvaluator.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/NoSuchResourceException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/PackageScanFailedException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/PageBinder.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Renderable.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Respond.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Show.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Shutdowner.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/SitebricksFilter.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/SitebricksInternalModule.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/SitebricksModule.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/SitebricksServletModule.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/StringBuilderRespond.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Template.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/TemplateLoader.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/TemplateLoadingException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/Visible.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/binding/ConcurrentPropertyCache.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/binding/CookieBasedFlashCache.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/binding/FlashCache.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/binding/GaeFlashCache.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/binding/HttpSessionFlashCache.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/binding/InvalidBindingException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/binding/MvelRequestBinder.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/binding/NoFlashCache.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/binding/PropertyCache.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/binding/RequestBinder.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/AnalysisError.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/AnalysisErrors.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/AnnotationNode.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/AnnotationParser.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/CompileError.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/CompileErrors.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/CompiledToken.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/Compilers.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/Dom.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/EvaluatorCompiler.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/ExpressionCompileException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/FlatTemplateCompiler.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/HtmlParser.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/HtmlTemplateCompiler.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/MvelEvaluatorCompiler.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/Parsing.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/RepeatToken.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/RequireWidgetInternPool.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/TemplateCompileException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/TemplateParseException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/Token.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/XmlTemplateCompiler.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/template/MvelTemplateCompiler.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/template/freemarker/FreemarkerTemplateCompiler.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/core/CaseWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/core/Repeat.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/core/ShowIf.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/core/package-info.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/debug/DebugPage.html create mode 100644 sitebricks/src/main/java/com/google/sitebricks/debug/DebugPage.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/headless/HeadlessRenderer.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/headless/Reply.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/headless/ReplyBasedHeadlessRenderer.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/headless/ReplyMaker.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/headless/Request.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/headless/Service.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/Delete.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/Get.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/Head.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/Post.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/Put.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/Select.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/Trace.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/negotiate/Accept.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ConnegModule.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ContentNegotiator.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ExactMatchNegotiator.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/http/negotiate/Negotiation.java create mode 100755 sitebricks/src/main/java/com/google/sitebricks/http/negotiate/RegexNegotiator.java create mode 100755 sitebricks/src/main/java/com/google/sitebricks/http/negotiate/WildcardNegotiator.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/i18n/Message.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/Attributes.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/Decorated.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/EmbedAs.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/SelfRendering.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/Strings.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/Templates.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/With.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/ArgumentWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/Chains.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/ChooseWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/DecorateWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/DefaultWidgetRegistry.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbedWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespond.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespondFactory.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/HeaderWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/IncludeWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/NoSuchWidgetException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/ProceedingWidgetChain.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/RawTextWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/RepeatWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/RequireWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/ShowIfWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/SingletonWidgetChain.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/TerminalWidgetChain.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/TextFieldWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/TextWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetChain.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetRegistry.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetWrapper.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/XmlDirectiveWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/control/XmlWidget.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/resource/Assets.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ClasspathResourcesService.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ResourceLoadingException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ResourcesService.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/Action.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/EventDispatchException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/InMemorySystemMetrics.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/InvalidEventHandlerException.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/PageBook.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/PathMatcher.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/PathMatcherChain.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/Production.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/RoutingDispatcher.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/ServiceAction.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/SpiAction.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/SystemMetrics.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java create mode 100644 sitebricks/src/main/resources/com/google/sitebricks/core/CaseWidget.html create mode 100644 sitebricks/src/main/resources/com/google/sitebricks/core/ll.gif create mode 100644 sitebricks/src/main/resources/com/google/sitebricks/core/lr.gif create mode 100644 sitebricks/src/main/resources/com/google/sitebricks/core/ul.gif create mode 100644 sitebricks/src/main/resources/com/google/sitebricks/core/ur.gif create mode 100644 sitebricks/src/main/resources/com/google/sitebricks/rendering/resource/mimetypes.properties create mode 100644 sitebricks/src/main/resources/com/google/sitebricks/templates.properties create mode 100644 sitebricks/src/test/java/com/google/sitebricks/EdslTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/LocalizationTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/RespondTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/RespondersForTesting.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/TemplateLoaderTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/WidgetFilterTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/binding/MvelRequestBinderTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/binding/PropertyCacheTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/compiler/FreemarkerTemplateCompilerTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/compiler/HtmlTemplateCompilerTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/compiler/XmlLineNumberParsingTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/compiler/XmlTemplateCompilerTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/headless/HeadlessReplyTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/headless/ReplyEdslTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/http/negotiate/ExactMatchNegotiatorTest.java create mode 100755 sitebricks/src/test/java/com/google/sitebricks/http/negotiate/RegexNegotiatorTest.java create mode 100755 sitebricks/src/test/java/com/google/sitebricks/http/negotiate/WildcardNegotiatorTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/DynTypedMvelEvaluatorCompiler.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/EvaluatorCompilerTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/MvelGenericsConfidenceTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/ParsingTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/control/ChooseWidgetTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/control/EmbedWidgetTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/control/EmbeddedRespondExtractorTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/control/HeaderWidgetTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/control/RepeatWidgetTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/control/RequireWidgetTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/control/ShowIfWidgetTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/control/TextFieldWidgetTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/control/TextWidgetTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/control/WidgetRegistryTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/resource/MimeTypesRegexIntegrationTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/rendering/resource/ResourcesServiceTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/routing/PageBookImplTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/routing/PathMatcherTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/routing/WidgetRoutingDispatcherTest.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/test/ContentNegotiationExample.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/test/Search.html create mode 100644 sitebricks/src/test/java/com/google/sitebricks/test/Search.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/test/Wiki.html create mode 100644 sitebricks/src/test/java/com/google/sitebricks/test/Wiki.java create mode 100644 sitebricks/src/test/java/com/google/sitebricks/util/TextToolsTest.java create mode 100644 sitebricks/src/test/resources/com/google/sitebricks/My.xml create mode 100644 sitebricks/src/test/resources/com/google/sitebricks/MyHtml.html create mode 100644 sitebricks/src/test/resources/com/google/sitebricks/MyXhtml.xhtml create mode 100644 sitebricks/src/test/resources/com/google/sitebricks/rendering/resource/my.xml create mode 100644 slf4j/pom.xml create mode 100644 slf4j/src/main/java/com/google/sitebricks/slf4j/Slf4jInjectionTypeListener.java create mode 100644 slf4j/src/main/java/com/google/sitebricks/slf4j/Slf4jModule.java create mode 100644 slf4j/src/test/java/com/google/sitebricks/slf4j/Slf4jIntegrationTest.java create mode 100644 stat/pom.xml create mode 100644 stat/src/main/java/com/google/sitebricks/stat/MemberAnnotatedWithAtStat.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/Stat.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatAnnotatedTypeListener.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatCollector.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatDescriptor.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatExposer.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatExposers.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatModule.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatReader.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatReaders.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatRegistrar.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/Stats.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatsPublisher.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatsPublishers.java create mode 100644 stat/src/main/java/com/google/sitebricks/stat/StatsServlet.java create mode 100644 stat/src/test/java/com/google/sitebricks/stat/StatExposersTest.java create mode 100644 stat/src/test/java/com/google/sitebricks/stat/StatReadersTest.java create mode 100644 stat/src/test/java/com/google/sitebricks/stat/StatsIntegrationTest.java create mode 100644 stat/src/test/java/com/google/sitebricks/stat/StatsPublishersTest.java create mode 100644 stat/src/test/java/com/google/sitebricks/stat/testservices/ChildDummyService.java create mode 100644 stat/src/test/java/com/google/sitebricks/stat/testservices/DummyService.java create mode 100644 stat/src/test/java/com/google/sitebricks/stat/testservices/StatExposerTestingService.java create mode 100644 stat/src/test/java/com/google/sitebricks/stat/testservices/StaticDummyService.java diff --git a/sitebricks-acceptance-tests/pom.xml b/sitebricks-acceptance-tests/pom.xml new file mode 100644 index 00000000..ed24efda --- /dev/null +++ b/sitebricks-acceptance-tests/pom.xml @@ -0,0 +1,137 @@ + + + 4.0.0 + + com.google.sitebricks + sitebricks-parent + 0.8.5 + + sitebricks-acceptance-tests + Sitebricks :: Acceptance Tests + + + + openqa-releases + OpenQA Releases + http://nexus.openqa.org/content/repositories/releases + + true + + + false + + + + codehaus + Codehaus Releases + http://repository.codehaus.org + + true + + + false + + + + + + + + org.slf4j + slf4j-api + 1.6.1 + + + + + + + org.mortbay.jetty + jetty + + + org.mortbay.jetty + jetty-util + + + org.mortbay.jetty + servlet-api-2.5 + + + org.seleniumhq.webdriver + webdriver-common + test + + + org.seleniumhq.webdriver + webdriver-support + test + + + org.seleniumhq.webdriver + webdriver-htmlunit + test + + + commons-collections + commons-collections + + + com.google.inject.extensions + guice-servlet + + + com.google.sitebricks + sitebricks + + + com.google.sitebricks + sitebricks-stat + ${project.version} + + + org.testng + testng + jdk15 + + + + + + + src/main/resources + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + org.apache.maven.plugins + maven-release-plugin + 2.0-beta-9 + + https://google-sitebricks.googlecode.com/svn/tags + + + + org.apache.maven.plugins + maven-jar-plugin + 2.3.1 + + + + test-jar + + + + + + + + diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Case.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Case.java new file mode 100644 index 00000000..0d44689b --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Case.java @@ -0,0 +1,19 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@At("/case") +public class Case { + private String color = "green"; + + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CompileErrors.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CompileErrors.java new file mode 100644 index 00000000..f080e35e --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CompileErrors.java @@ -0,0 +1,21 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@At("/error") +public class CompileErrors { + public String getMessage1() { + return ""; + } + + public int getMessage2() { + return 0; + } + + public String getMessage3() { + throw new RuntimeException("an exception"); + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/ContentNegotiation.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/ContentNegotiation.java new file mode 100644 index 00000000..40b13da2 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/ContentNegotiation.java @@ -0,0 +1,29 @@ +package com.google.sitebricks.example; + + +import com.google.sitebricks.At; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.negotiate.Accept; + + +@At("/conneg") +public class ContentNegotiation { + + // By default we serve gif, hehehehe. + private String content = "GIF"; + + @Get @Accept("image/jpeg") + public void jpeg() { + content = "JPEG"; + } + + + @Get @Accept("image/png") + public void png() { + content = "PNG"; + } + + public String getContent() { + return content; + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Conversion.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Conversion.java new file mode 100644 index 00000000..0f4bcfe0 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Conversion.java @@ -0,0 +1,48 @@ +package com.google.sitebricks.example; + +import java.util.Calendar; +import java.util.Date; + +import com.google.sitebricks.At; + +@At("/conversion") +public class Conversion { + private Date date; + private Calendar calendar; + private String message; + private Double dbl; + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public Calendar getCalendar() { + return calendar; + } + public void setCalendar(Calendar calendar) { + this.calendar = calendar; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Double getDbl() { + return dbl; + } + + public void setDbl(Double dbl) { + this.dbl = dbl; + } + + + +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DecoratedPage.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DecoratedPage.java new file mode 100644 index 00000000..d531f5bc --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DecoratedPage.java @@ -0,0 +1,16 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.rendering.Decorated; + +@Decorated +public class DecoratedPage extends DecoratorPage { + + @Override + public String getWorld() { + return "This comes from the subclass"; + } + + public String getDescription() { + return "very cool"; + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DecoratorPage.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DecoratorPage.java new file mode 100644 index 00000000..3427484c --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DecoratorPage.java @@ -0,0 +1,12 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.Show; + +@Show("/Decorator.html") +public abstract class DecoratorPage { + public String getHello() { + return "Hello (from the superclass)"; + } + + public abstract String getWorld(); +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DynamicJs.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DynamicJs.java new file mode 100644 index 00000000..a50c1ade --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/DynamicJs.java @@ -0,0 +1,17 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; +import com.google.sitebricks.Show; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@At("/dynamic.js") @Show("dynamic.js") +public class DynamicJs { + public static final String A_MESSAGE = "Hi from google-sitebricks! (this message" + + " was dynamically generated =)"; + + public String getMessage() { + return A_MESSAGE; + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Embed.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Embed.java new file mode 100644 index 00000000..994129db --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Embed.java @@ -0,0 +1,23 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@At("/embed") +public class Embed { + + private List arg = Arrays.asList( + "Embedding in google-sitebricks is awesome!", + "Embedding in google-sitebricks totally rules!", + "google-sitebricks embed FTW!" + ); + + public List getArg() { + return arg; + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Forms.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Forms.java new file mode 100644 index 00000000..7e09ceb8 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Forms.java @@ -0,0 +1,40 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@At("/forms") +public class Forms { + private String text = "initial textfield value"; + private String chosen = "(nothing)"; + private List autobots = Arrays.asList("Bumblebee", "Ultra Magnus", "Optimus Prime", "Kup", "Hot Rod"); + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public List getAutobots() { + return autobots; + } + + public void setAutobots(List autobots) { + this.autobots = autobots; + } + + public String getChosen() { + return chosen; + } + + public void setChosen(String chosen) { + this.chosen = chosen; + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HelloWorld.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HelloWorld.java new file mode 100644 index 00000000..8f5cc20c --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HelloWorld.java @@ -0,0 +1,27 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; +import com.google.sitebricks.rendering.EmbedAs; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@At("/hello") @EmbedAs("Hello") +public class HelloWorld { + public static volatile String HELLO_MSG = "Hello from google-sitebricks!"; + + private String message = HELLO_MSG; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + // Some deterministic mangled representation of the input. + public String mangle(String s) { + return "" + s.hashCode(); + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HelloWorldService.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HelloWorldService.java new file mode 100644 index 00000000..08657a07 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HelloWorldService.java @@ -0,0 +1,48 @@ +package com.google.sitebricks.example; + +import com.google.inject.Inject; +import com.google.sitebricks.At; +import com.google.sitebricks.Show; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.headless.Service; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.rendering.Templates; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Show("HelloWorld.html") @Service +public class HelloWorldService { + public static final String HELLO_MSG = "Hello from google-sitebricks!"; + + private final Templates templates; + private String message = HELLO_MSG; + + @Inject + public HelloWorldService(Templates templates) { + this.templates = templates; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + // Some deterministic mangled representation of the input. + public String mangle(String s) { + return "" + s.hashCode(); + } + + @Get + public Reply get() { + return Reply.with(this).template(HelloWorldService.class); + } + + @At("/direct") @Get + public Reply getDirect() { + return Reply.with(templates.render(HelloWorldService.class, this)); + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HiddenFieldMethod.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HiddenFieldMethod.java new file mode 100644 index 00000000..bd5fb6f7 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/HiddenFieldMethod.java @@ -0,0 +1,43 @@ +package com.google.sitebricks.example; + + +import com.google.sitebricks.At; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.http.Put; + + +/** + * @author Peter Knego + */ +@At("/hiddenfieldmethod") +public class HiddenFieldMethod { + private String text = "initial textfield value"; + + private String putMessage = ""; + + public String getPutMessage() { + return putMessage; + } + + public void setPutMessage(String putMessage) { + this.putMessage = putMessage; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + @Put + public void put() { + putMessage = "Submitted via PUT"; + } + + @Post + public void post() { + putMessage = "Submitted via POST"; + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/I18n.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/I18n.java new file mode 100644 index 00000000..c35f54a4 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/I18n.java @@ -0,0 +1,67 @@ +package com.google.sitebricks.example; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import com.google.sitebricks.At; +import com.google.sitebricks.i18n.Message; + +import javax.servlet.http.HttpServletRequest; +import java.util.Locale; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@At("/i18n") +public class I18n { + // These would typically be provided by a translated set resource bundle (external). + public static final String HELLO = "hello"; + public static final String HELLO_IN_FRENCH = "Bonjour misieu ${person}!"; + + + private final MyMessages messages; + private final HttpServletRequest request; + + private String name; + + @Inject + public I18n(HttpServletRequest request, MyMessages messages) { + this.messages = messages; + this.request = request; + } + + + public String getMessage() { + // only evaluate our message if the user has entered her name. + return null == name ? "" : messages.hello(name); + } + + public void setName(String name) { + this.name = name; + } + + public Locale getLocale() { + return request.getLocale(); + } + + /** + * This is the i18N message interface. By default we use the messages provided + * in the annotation. You can customize these by using the localize() rule in + * your sitebricks module with different resource bundles and locales. + * + * Note that these messages can be given arguments and even exposed directly to + * your template if you so wish, you have to invoke the method with parens like + * so: + * + * + * + * ${messages.hello("Friend")} + * + * Will invoke the hello() method below with a string argument "Friend". At runtime + * this will produce localized output in whatever locale the browser requests and + * is available in your translation sets. + */ + public static interface MyMessages { + @Message(message = "Hello there ${person}!") + String hello(@Named("person") String name); + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/MvelTemplateExample.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/MvelTemplateExample.java new file mode 100644 index 00000000..261a841c --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/MvelTemplateExample.java @@ -0,0 +1,17 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; +import com.google.sitebricks.Show; + +/** + * Example of a page rendered with a different templating technology + * (other than sitebricks). + */ +@At("/template/mvel") @Show("MvelTemplateExample.mvel") +public class MvelTemplateExample { + private final String message = "hey hey! Generated by MVEL templates dynamically =)"; + + public String getMessage() { + return message; + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/NextPage.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/NextPage.java new file mode 100644 index 00000000..735bf64b --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/NextPage.java @@ -0,0 +1,21 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; + +/** + * The target page that receives the state. + * + * @author dhanji@google.com (Dhanji R. Prasanna) + */ +@At("/nextpage") +public class NextPage { + private String persistedValue; + + public NextPage(String persistedValue) { + this.persistedValue = persistedValue; + } + + public String getPersistedValue() { + return persistedValue; + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PageChain.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PageChain.java new file mode 100644 index 00000000..aa8d952b --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PageChain.java @@ -0,0 +1,26 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; +import com.google.sitebricks.http.Post; + +/** + * Demonstrates passing state between pages, without + * leaking it to the client or using a persistent datastore. + * + * @author dhanji@google.com (Dhanji R. Prasanna) + */ +@At("/pagechain") +public class PageChain { + private String userValue; + + @Post NextPage redirect() { + + // Redirect to nextpage and use this provided instance, + // that way we pass the custom value thru. + return new NextPage(userValue); + } + + public void setUserValue(String userValue) { + this.userValue = userValue; + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PostableRestfulWebService.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PostableRestfulWebService.java new file mode 100644 index 00000000..ae94f6a8 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/PostableRestfulWebService.java @@ -0,0 +1,40 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.headless.Request; +import com.google.sitebricks.headless.Service; +import com.google.sitebricks.http.Post; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.Map; + +/** + * Lets you send JSON data. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@At("/postable") @Service +public class PostableRestfulWebService { + + @Post + public Reply postBook(HttpServletRequest servletRequest, Request request) { + RestfulWebService.Book perdido = request.read(RestfulWebService.Book.class).as(Json.class); + + boolean assertions = RestfulWebService.PERDIDO_STREET_STATION.equals(perdido.getName()) + && RestfulWebService.CHINA_MIEVILLE.equals(perdido.getAuthor()) + && RestfulWebService.PAGE_COUNT == perdido.getPageCount(); + + // Assert the request params are legit. + @SuppressWarnings("unchecked") + Map map = servletRequest.getParameterMap(); + for (Map.Entry entry : map.entrySet()) { + assertions = assertions + && Arrays.asList(entry.getValue()).equals(request.params().get(entry.getKey())); + } + + return assertions ? Reply.with(perdido.getAuthor()) : Reply.with("failed"); + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Repeat.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Repeat.java new file mode 100644 index 00000000..e2e51632 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Repeat.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; + +import java.util.*; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@At("/repeat") +public class Repeat { + private static final List NAMES = Arrays.asList("Dhanji", "Josh", "Jody", "Iron Man"); + + //property returns a list of names + public List getNames() { + return NAMES; + } + + //try a set this time, returns movies (to demo nested repeat) + public Set getMovies() { + return new HashSet(Arrays.asList(new Movie(), new Movie(), new Movie())); + } + + public static class Movie { + + //try a collection this time. same as property Repeat.getNames() from the outer class + public Collection getActors() { + return NAMES; + } + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebService.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebService.java new file mode 100644 index 00000000..0807adca --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebService.java @@ -0,0 +1,83 @@ +package com.google.sitebricks.example; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Injector; +import com.google.sitebricks.At; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.headless.Service; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@At("/service") @Service +public class RestfulWebService { + public static final String PERDIDO_STREET_STATION = "Perdido Street Station"; + public static final String CHINA_MIEVILLE = "China Mieville"; + public static final int PAGE_COUNT = 789; + + @Get + public Reply books(Injector injector, HttpServletRequest request, + @SitebricksConfig.Test Start start) { + Preconditions.checkNotNull(injector, "method argument injection failed"); + Preconditions.checkNotNull(request, "method argument injection failed"); + Preconditions.checkNotNull(start, "method argument injection failed"); + + Book perdido = new Book(); + perdido.setAuthor(CHINA_MIEVILLE); + perdido.setName(PERDIDO_STREET_STATION); + perdido.setPageCount(PAGE_COUNT); + + return Reply.with(perdido) + .headers(ImmutableMap.of("hi", "there")) + .type("application/json") + .as(Json.class); + } + + @Post + public Reply redirect() { + return Reply.saying() + .redirect("/other"); + } + + + /** + * A data model object, or "Entity" that we will use to + * generate the reply. This can be any Java object and + * typically will not be an inner class like this one. + */ + public static class Book { + private String name; + private String author; + private int pageCount; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public int getPageCount() { + return pageCount; + } + + public void setPageCount(int pageCount) { + this.pageCount = pageCount; + } + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceNoAnnotations.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceNoAnnotations.java new file mode 100644 index 00000000..22ec63c7 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceNoAnnotations.java @@ -0,0 +1,40 @@ +package com.google.sitebricks.example; + +import com.google.common.collect.ImmutableMap; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; + +/** + * Used to ensure that the configuration works the same even without + * annotations. We have this extra test coz some logic that distinguishes + * web services from normal web pages relies on the presence of annotations + * (failing explicit module config), and this ensures nothing goes haywire. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class RestfulWebServiceNoAnnotations { + public static final String PERDIDO_STREET_STATION = "Perdido Street Station"; + public static final String CHINA_MIEVILLE = "China Mieville"; + public static final int PAGE_COUNT = 789; + + @Get + public Reply books() { + RestfulWebService.Book perdido = new RestfulWebService.Book(); + perdido.setAuthor(CHINA_MIEVILLE); + perdido.setName(PERDIDO_STREET_STATION); + perdido.setPageCount(PAGE_COUNT); + + return Reply.with(perdido) + .headers(ImmutableMap.of("hi", "there")) + .type("application/json") + .as(Json.class); + } + + @Post + public Reply redirect() { + return Reply.saying() + .redirect("/other"); + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithCRUD.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithCRUD.java new file mode 100644 index 00000000..b24e3e44 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithCRUD.java @@ -0,0 +1,61 @@ +package com.google.sitebricks.example; + +import com.google.inject.name.Named; +import com.google.sitebricks.At; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.headless.Request; +import com.google.sitebricks.headless.Service; +import com.google.sitebricks.http.Delete; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.http.Put; + +/** + * Demonstrates CRUD operations in a restful webservice. + * + * // ------------------------------ + * // Method URL Action + * // ------------------------------ + * // POST /user CREATE + * // GET /user READ (collection) + * // GET /user/1 READ (individual) + * // PUT /user/1 UPDATE + * // DELETE /user/1 DELETE + * + * @author Jason van Zyl + */ +@At("/json/:type") @Service +public class RestfulWebServiceWithCRUD { + public static final String TYPE = "user"; + public static final String BASE_SERVICE_PATH = "/json/" + TYPE; + public static final String CREATE = "CREATE"; + public static final String READ_COLLECTION = "READ_COLLECTION"; + public static final String READ_INDIVIDUAL = "READ_INDIVIDUAL"; + public static final String UPDATE = "UPDATE"; + public static final String DELETE = "DELETE"; + + @Post + public Reply post( Request request, @Named( "type" ) String type ) { + return Reply.with(CREATE); + } + + @Get + public Reply get( @Named( "type" ) String type ) { + return Reply.with(READ_COLLECTION); + } + + @At( "/:id" ) @Get + public Reply get( @Named( "type" ) String type, @Named( "id" ) String id ) { + return Reply.with(READ_INDIVIDUAL); + } + + @At( "/:id" ) @Put + public Reply put( Request request, @Named( "type" ) String type, @Named( "id" ) String id ) { + return Reply.with(UPDATE); + } + + @At( "/:id" ) @Delete + public Reply delete( @Named( "type" ) String type, @Named( "id" ) String id ) { + return Reply.with(DELETE); + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithCRUDConversions.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithCRUDConversions.java new file mode 100644 index 00000000..b1c5248f --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithCRUDConversions.java @@ -0,0 +1,196 @@ +package com.google.sitebricks.example; + +import java.io.Serializable; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.google.inject.name.Named; +import com.google.sitebricks.At; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.headless.Request; +import com.google.sitebricks.headless.Service; +import com.google.sitebricks.http.Delete; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.http.Put; + +@At(RestfulWebServiceWithCRUDConversions.AT_ME) +@Service +public class RestfulWebServiceWithCRUDConversions { + public static final String AT_ME = "/jsonConversion"; + public static List widgets = new ArrayList(); + + static { + populate(); + } + + private static void populate() { + SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy HH:mm"); + try { + addWidget(new Widget(1, "Widget 1", sdf.parse("01-JAN-1990 12:00"), 1.50)); + addWidget(new Widget(2, "Widget 2", sdf.parse("02-FEB-2000 18:00"), 21.75)); + addWidget(new Widget(3, "Widget 3", sdf.parse("03-MAR-2010 23:00"), 19.99)); + } catch (Exception e) { + } + } + + @Post + public Reply post(Request request) { + Widget widget = request.read(Widget.class).as(Json.class); + addWidget(widget); + return Reply.with(widget).as(Json.class).type("application/json"); + } + + @Put + public Reply put(Request request) { + Widget widget = request.read(Widget.class).as(Json.class); + addWidget(widget); + return Reply.with(widget).as(Json.class).type("application/json"); + } + + @Get + public Reply> getAll() { + return Reply.with(widgets).as(Json.class).type("application/json"); + } + + @At("/:id") + @Get + public Reply get(@Named("id") int id) { + Widget widget = findWidget(id); + if (widget != null) + return Reply.with(widget).as(Json.class).type("application/json"); + return Reply.saying().error(); + } + + @At("/:id") + @Delete + public Reply delete(@Named("id") int id) { + Widget widget = removeWidget(id); + if (widget != null) + return Reply.with(widget).as(Json.class).type("application/json"); + return Reply.saying().error(); + } + + public static Widget removeWidget(Widget widget) { + return removeWidget(widget.getId()); + } + + public static Widget removeWidget(int id) { + Widget oldWidget = findWidget(id); + if (oldWidget != null) + widgets.remove(oldWidget); + return oldWidget; + } + + public static void addWidget(Widget widget) { + Widget oldWidget = findWidget(widget.getId()); + if (oldWidget != null) + widgets.remove(oldWidget); + widgets.add(widget); + } + + public static Widget findWidget(int id) { + for (Widget widget : widgets) { + if (widget.getId() == id) + return widget; + } + return null; + } + + public static class Widget implements Serializable { + int id; + String name; + Date available; + double price; + + public Widget() { + } + + public Widget(int id, String name, Date available, double price) { + this.id = id; + this.name = name; + this.available = available; + this.price = price; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getAvailable() { + return available; + } + + public void setAvailable(Date available) { + this.available = available; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public Widget clone() { + return new Widget (id, name, available, price); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((available == null) ? 0 : available.hashCode()); + result = prime * result + id; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + long temp; + temp = Double.doubleToLongBits(price); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Widget other = (Widget) obj; + if (available == null) { + if (other.available != null) + return false; + } else if (!available.equals(other.available)) + return false; + if (id != other.id) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (Double.doubleToLongBits(price) != Double.doubleToLongBits(other.price)) + return false; + return true; + } + + + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithMatrixParams.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithMatrixParams.java new file mode 100644 index 00000000..c82661ad --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithMatrixParams.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.example; + +import com.google.inject.name.Named; +import com.google.sitebricks.At; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.headless.Request; +import com.google.sitebricks.headless.Service; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; + +/** + * Demonstrates subpaths in a restful webservice. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@At("/matrixpath") @Service +public class RestfulWebServiceWithMatrixParams { + public static final String TOPLEVEL = "toplevel_m"; + public static final String PATH_1 = "path1"; + + @Get + public Reply topLevel() { + return Reply.with(TOPLEVEL); + } + + @At("/:variable/:id") @Post + public Reply variableSecondLevel(@Named("variable") String arg, @Named("id") String id, + Request request) { + return Reply.with(request.matrix().toString() + "_" + arg + "_" + id); + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithSubpaths.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithSubpaths.java new file mode 100644 index 00000000..62b804d0 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithSubpaths.java @@ -0,0 +1,56 @@ +package com.google.sitebricks.example; + +import com.google.inject.name.Named; +import com.google.sitebricks.At; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.headless.Service; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; + +/** + * Demonstrates subpaths in a restful webservice. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@At("/superpath") @Service +public class RestfulWebServiceWithSubpaths { + public static final String TOPLEVEL = "toplevel"; + public static final String PATH_1 = "path1"; + public static final String PATH_2 = "path2"; + public static final String PATH_3 = "path3"; + + @Get + public Reply topLevel() { + return Reply.with(TOPLEVEL); + } + + @At("/subpath1") @Post + public Reply path1() { + return Reply.with(PATH_1); + } + + @At("/subpath2") @Post + public Reply path2() { + return Reply.with(PATH_2); + } + + @At("/subpath3") @Post + public Reply path3() { + return Reply.with(PATH_3); + } + + @At("/subpath1/:variable") @Post + public Reply variable(@Named("variable") String arg) { + return Reply.with(arg); + } + + @At("/subpath1/:variable/:id") @Post + public Reply variableSecondLevel(@Named("variable") String arg, @Named("id") String id) { + return Reply.with(arg + "_" + id); + } + + @At("/subpath3/:sec") @Post + public Reply variableSubpath2(@Named("sec") String arg) { + return Reply.with(arg); + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithSubpaths2.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithSubpaths2.java new file mode 100644 index 00000000..70307e5b --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/RestfulWebServiceWithSubpaths2.java @@ -0,0 +1,98 @@ +package com.google.sitebricks.example; + +import com.google.inject.name.Named; +import com.google.sitebricks.At; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.headless.Service; +import com.google.sitebricks.http.Delete; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.http.Put; + +/** + * Demonstrates subpaths in a restful webservice. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@At("/superpath2/:dynamic") @Service +public class RestfulWebServiceWithSubpaths2 { + public static final String TOPLEVEL = "test1"; + public static final String PATH_1 = "path1"; + public static final String PATH_1_PUT = "path1put"; + public static final String PATH_1_DELETE = "path1delete"; + + @Get + public Reply topLevel(@Named("dynamic") String dynamic) { + return Reply.with(dynamic); + } + + @At("/subpath1") @Post + public Reply path1(@Named("dynamic") String dynamic) { + return Reply.with(PATH_1); + } + + @At("/subpath1") @Put + public Reply path1Put(@Named("dynamic") String dynamic) { + return Reply.with(PATH_1_PUT); + } + + @At("/subpath1") @Delete + public Reply path1Delete(@Named("dynamic") String dynamic) { + return Reply.with(PATH_1_DELETE); + } + + @At("/:second") @Post + public Reply path2(@Named("dynamic") String dynamic, @Named("second") String second) { + return Reply.with(dynamic + "_" + second); + } + + @At("/:second") @Delete + public Reply path2Delete(@Named("dynamic") String dynamic, + @Named("second") String second) { + return Reply.with("delete:" + dynamic + "_" + second); + } + + @At("/:l2/:l3") @Delete + public Reply l2l3(@Named("dynamic") String dynamic, + @Named("l2") String second, + @Named("l3") String third) { + return Reply.with("delete:" + dynamic + "_" + second + "_" + third); + } + + @At("/:l2/:l3") @Put + public Reply l2l3Put(@Named("dynamic") String dynamic, + @Named("l2") String second, + @Named("l3") String third) { + return Reply.with("put:" + dynamic + "_" + second + "_" + third); + } + + @At("/:l2/:l3") @Post + public Reply l2l3Post(@Named("dynamic") String dynamic, + @Named("l2") String second, + @Named("l3") String third) { + return Reply.with("post:" + dynamic + "_" + second + "_" + third); + } + + @At("/:l2/:l3") @Get + public Reply l2l3Get(@Named("dynamic") String dynamic, + @Named("l2") String second, + @Named("l3") String third) { + return Reply.with("get:" + dynamic + "_" + second + "_" + third); + } + +// BUG EXISTS WHEN 4-Level MIXED PATHS ARE INTRODUCED: + + @At("/:l2/:l3/l4") @Get + public Reply l4Get(@Named("dynamic") String dynamic, + @Named("l2") String second, + @Named("l3") String third) { + return Reply.with("4l:get:" + dynamic + "_" + second + "_" + third); + } + + @At("/:l2/:l3/l4") @Post + public Reply l4Post(@Named("dynamic") String dynamic, + @Named("l2") String second, + @Named("l3") String third) { + return Reply.with("4l:post:" + dynamic + "_" + second + "_" + third); + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SelectRouting.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SelectRouting.java new file mode 100644 index 00000000..391899b2 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SelectRouting.java @@ -0,0 +1,118 @@ +package com.google.sitebricks.example; + + +import com.google.sitebricks.At; +import com.google.sitebricks.http.Delete; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.http.Put; +import com.google.sitebricks.http.Select; + +import java.util.ArrayList; +import java.util.List; + + +@At("/select") @Select("event") +public class SelectRouting { + + private List data = new ArrayList(); + + public SelectRouting() { + } + + public SelectRouting(List data) { + this.data = data; + } + + public List getData() { + return data; + } + + public void setData(List data) { + this.data = data; + } + + @Post + public void defaultPost() { + data.add("defaultPost"); + } + + @Post("foo") + public void fooPost() { + data.add("fooPost"); + } + + @Post("bar") + public void barPost() { + data.add("barPost"); + } + + @Post("304") + public Object redirectPost() { + data.add("redirectPost"); + return new SelectRouting(data); + } + + @Get + public void defaultGet() { + data.add("defaultGet"); + } + + @Get("foo") + public void fooGet() { + data.add("fooGet"); + } + + @Get("bar") + public void barGet() { + data.add("barGet"); + } + + @Get("304") + public Object redirectGet() { + data.add("redirectGet"); + return new SelectRouting(data); + } + + @Put + public void defaultPut() { + data.add("defaultPut"); + } + + @Put("foo") + public void fooPut() { + data.add("fooPut"); + } + + @Put("bar") + public void barPut() { + data.add("barPut"); + } + + @Put("304") + public Object redirectPut() { + data.add("redirectPut"); + return new SelectRouting(data); + } + + @Delete + public void defaultDelete() { + data.add("defaultDelete"); + } + + @Delete("foo") + public void fooDelete() { + data.add("fooDelete"); + } + + @Delete("bar") + public void barDelete() { + data.add("barDelete"); + } + + @Delete("304") + public Object redirectDelete() { + data.add("redirectDelete"); + return new SelectRouting(data); + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/ShowIf.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/ShowIf.java new file mode 100644 index 00000000..5a32ac66 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/ShowIf.java @@ -0,0 +1,24 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@At("/showif") +public class ShowIf { + private boolean show; + private String message = "Hello from google-sitebricks!"; + + public String getMessage() { + return message; + } + + public boolean isShow() { + return show; + } + + public void setShow(boolean show) { + this.show = show; + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java new file mode 100644 index 00000000..fdb8d17b --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java @@ -0,0 +1,145 @@ +package com.google.sitebricks.example; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.BindingAnnotation; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Singleton; +import com.google.inject.Stage; +import com.google.inject.servlet.GuiceServletContextListener; +import com.google.sitebricks.stat.StatModule; +import com.google.sitebricks.AwareModule; +import com.google.sitebricks.SitebricksModule; +import com.google.sitebricks.binding.FlashCache; +import com.google.sitebricks.binding.HttpSessionFlashCache; +import com.google.sitebricks.conversion.DateConverters; +import com.google.sitebricks.debug.DebugPage; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.routing.Action; + +import javax.servlet.http.HttpServletRequest; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Locale; +import java.util.Map; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class SitebricksConfig extends GuiceServletContextListener { + + // a weird format + public static final String DEFAULT_DATE_TIME_FORMAT = "dd MM yy SS"; + +@Override + protected Injector getInjector() { + return Guice.createInjector(Stage.DEVELOPMENT, new SitebricksModule() { + + @Override + protected void configureSitebricks() { + + // TODO(dhanji): find a way to run the suite again with this module installed. +// install(new GaeModule()); + + bind(FlashCache.class).to(HttpSessionFlashCache.class).in(Singleton.class); + + // TODO We should run the test suite once with this turned off and with scan() on. +// scan(SitebricksConfig.class.getPackage()); + bindExplicitly(); + bindActions(); + + at("/no_annotations/service").serve(RestfulWebServiceNoAnnotations.class); + at("/debug").show(DebugPage.class); + + bind(Start.class).annotatedWith(Test.class).to(Start.class); + + // Localize using the default translation set (i.e. from the @Message annotations) + localize(I18n.MyMessages.class).usingDefault(); + localize(I18n.MyMessages.class).using(Locale.CANADA_FRENCH, + ImmutableMap.of(I18n.HELLO, I18n.HELLO_IN_FRENCH)); + + install(new StatModule("/stats")); + + converter(new DateConverters.DateStringConverter(DEFAULT_DATE_TIME_FORMAT)); + + install(new AwareModule() { + @Override + protected void configureLifecycle() { + observe(StartAware.class).asEagerSingleton(); + } + }); + } + + private void bindExplicitly() { + //TODO explicit bindings should override scanned ones. + at("/").show(Start.class); + at("/hello").show(HelloWorld.class); + at("/case").show(Case.class); + at("/embed").show(Embed.class); + at("/error").show(CompileErrors.class); + at("/forms").show(Forms.class); + at("/repeat").show(Repeat.class); + at("/showif").show(ShowIf.class); + at("/dynamic.js").show(DynamicJs.class); + + at("/conversion").show(Conversion.class); + + at("/hiddenfieldmethod").show(HiddenFieldMethod.class); + at("/select").show(SelectRouting.class); + at("/conneg").show(ContentNegotiation.class); + + at("/helloservice").serve(HelloWorldService.class); + + at("/service").serve(RestfulWebService.class); + at("/postable").serve(PostableRestfulWebService.class); + at("/superpath").serve(RestfulWebServiceWithSubpaths.class); + at("/matrixpath").serve(RestfulWebServiceWithMatrixParams.class); + at("/superpath2/:dynamic").serve(RestfulWebServiceWithSubpaths2.class); + at("/json/:type").serve(RestfulWebServiceWithCRUD.class); + at("/jsonConversion").serve(RestfulWebServiceWithCRUDConversions.class); + + at("/pagechain").show(PageChain.class); + at("/nextpage").show(NextPage.class); + + at("/i18n").show(I18n.class); + + // MVEL template. + at("/template/mvel").show(MvelTemplateExample.class); + + // templating by extension + at("/template").show(DecoratedPage.class); + + embed(HelloWorld.class).as("Hello"); + } + + private void bindActions() { + at("/spi/test") + .perform(action("get:top")) + .on(Get.class) + .perform(action("post:junk_subpath1")) + .on(Post.class); + } + }); + } + + private Action action(final String reply) { + return new Action() { + @Override + public boolean shouldCall(HttpServletRequest request) { + return true; + } + + @Override + public Object call(Object page, Map map) { + return Reply.with(reply); + } + }; + } + + @BindingAnnotation + @Retention(RetentionPolicy.RUNTIME) + public static @interface Test { + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Start.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Start.java new file mode 100644 index 00000000..411ea726 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Start.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.example; + +import com.google.inject.Singleton; +import com.google.sitebricks.stat.Stat; +import com.google.sitebricks.At; +import com.google.sitebricks.Show; +import com.google.sitebricks.http.Get; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@At("/") +@Show("index.html") @Singleton +public class Start { + public static final String PAGE_LOADS = "page-loads"; + public static volatile String HELLO_MSG = "YOU SHOULD NEVER SEE THIS!"; + private String message = HELLO_MSG; + + @Stat(PAGE_LOADS) + private final AtomicInteger pageLoads = new AtomicInteger(); + + public String getMessage() { + return message; + } + + @Get void display() { + pageLoads.incrementAndGet(); + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/StartAware.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/StartAware.java new file mode 100644 index 00000000..0b696a52 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/StartAware.java @@ -0,0 +1,17 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.Aware; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class StartAware implements Aware { + @Override + public void startup() { + HelloWorld.HELLO_MSG = "Hello from google-sitebricks!"; + } + + @Override + public void shutdown() { + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/TestPage.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/TestPage.java new file mode 100644 index 00000000..86d50d96 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/TestPage.java @@ -0,0 +1,28 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@At("/test") +public class TestPage { + private String message = "hello"; + private boolean appear = true; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public boolean isAppear() { + return appear; + } + + public void setAppear(boolean appear) { + this.appear = appear; + } +} diff --git a/sitebricks-acceptance-tests/src/main/resources/Case.html b/sitebricks-acceptance-tests/src/main/resources/Case.html new file mode 100644 index 00000000..c3c34bf6 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/Case.html @@ -0,0 +1,71 @@ + + + + + + + + + + +
+ +
+
+

examples

+
+ + Example of the @Case widget, which is similar to a java switch() statement (we use it to choose between 3 colors):
+ + @Case(choice=color) + + + @When("red") + + Red + + + @When("yellow") + + Yellow + + + @When("green") + + Green + + + + + +
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/CompileErrors.html b/sitebricks-acceptance-tests/src/main/resources/CompileErrors.html new file mode 100644 index 00000000..0f6df653 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/CompileErrors.html @@ -0,0 +1,61 @@ + + + + + @Require + + + @Require + + + + + + + + + +
+ +
+
+

examples

+ +
+ + Message : ${message 3} +
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/ContentNegotiation.html b/sitebricks-acceptance-tests/src/main/resources/ContentNegotiation.html new file mode 100644 index 00000000..734a20d7 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/ContentNegotiation.html @@ -0,0 +1,8 @@ + + +
+ ${content} +
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/Conversion.html b/sitebricks-acceptance-tests/src/main/resources/Conversion.html new file mode 100644 index 00000000..6623d5c6 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/Conversion.html @@ -0,0 +1,73 @@ + + + + + + + + + + +
+ +
+
+

examples

+ +
+ + The following demonstrates type conversion and binding to page object properties. +

+ + Bound: +

+ datefield: ${date} +

+ +

+ calendarfield: ${calendar} +

+ +

+ messagefield: ${message} +

+ +

+ doublefield: ${dbl} +

+
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/DecoratedPage.html b/sitebricks-acceptance-tests/src/main/resources/DecoratedPage.html new file mode 100644 index 00000000..f5a0d690 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/DecoratedPage.html @@ -0,0 +1,7 @@ + + + This is in the extension + + The extension is ${description} + + \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/main/resources/Decorator.html b/sitebricks-acceptance-tests/src/main/resources/Decorator.html new file mode 100644 index 00000000..fa0461f0 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/Decorator.html @@ -0,0 +1,19 @@ + + + + Text defined in the base class +
${hello}
+ +
+ + Text defined in the subclass +
${world}
+ +
+ + @Decorated +
This should be replaced by the extension
+ + This is just in the template + + \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/main/resources/Embed.html b/sitebricks-acceptance-tests/src/main/resources/Embed.html new file mode 100644 index 00000000..c7f2abfe --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/Embed.html @@ -0,0 +1,48 @@ + + + + + + + + + The following is a custom widget created from the HelloWorld example. + Notice that the stylesheet is available in the enclosing page via use of the @Require widget + (this page is not styled by default, whereas HelloWorld is). +

+ + We can even pass arguments to the embedded page (let's use the list "${arg}" instead of its + default message). + +



+ + @Repeat(var="item", items=arg) +
+ @Hello(message=item) + + This text is replaced by the embed. + +
+



+



+ +

+

Let's try that again!!

+

+



+ + @Repeat(var="item", items=arg) +
+ @Hello(message=item) + + This text is replaced by the embed. + +
+ + diff --git a/sitebricks-acceptance-tests/src/main/resources/Forms.html b/sitebricks-acceptance-tests/src/main/resources/Forms.html new file mode 100644 index 00000000..2b08d100 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/Forms.html @@ -0,0 +1,88 @@ + + + + + + + + + + +
+ +
+
+

examples

+ +
+ + The following demonstrates forms and binding to page object properties. +

+ +
+ + + +
+
+ + Name some autobots: +
+
+
+ + + + + +
+ +


+ Bound: +

+ textfield: ${text} +

+ +

+ autobots: ${autobots} +

+ +

+ +

+
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/HelloWorld.html b/sitebricks-acceptance-tests/src/main/resources/HelloWorld.html new file mode 100644 index 00000000..8c0a3560 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/HelloWorld.html @@ -0,0 +1,67 @@ + + + + + + @Require + + + @Require + + + + + + + + + +
+ +
+
+

examples

+ +
+ + Message : ${message} +
+ +
+ Call a function: ${this.mangle(message)} +
+ +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/HiddenFieldMethod.html b/sitebricks-acceptance-tests/src/main/resources/HiddenFieldMethod.html new file mode 100644 index 00000000..6e55ee40 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/HiddenFieldMethod.html @@ -0,0 +1,83 @@ + + + + + + + +start header + + +
+ +
+
+

examples

+ +
+ + The following demonstrates a form with HTTP PUT method simulated via a hidden field.. +

+ +
+ + + + + + +
+ +
+ Hint: POST button removes hidden field via javascript and then submits normally via POST +
+ Bound: +

+ textfield: ${text} +

+ Message: ${putMessage} +
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/I18n.html b/sitebricks-acceptance-tests/src/main/resources/I18n.html new file mode 100644 index 00000000..509a2ccd --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/I18n.html @@ -0,0 +1,68 @@ + + + + + @Require + + + @Require + + + + + + + + + +
+ +
+
+

examples

+
+ + Browser Locale: ${locale} +
+ +
+ Enter name: + +
+
+ + Localized message: ${message} +
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/MvelTemplateExample.mvel b/sitebricks-acceptance-tests/src/main/resources/MvelTemplateExample.mvel new file mode 100644 index 00000000..eb611d08 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/MvelTemplateExample.mvel @@ -0,0 +1,52 @@ + + + + + + + + + +
+ +
+
+

examples

+
+ + Message : @{message} +
(Rendered using MVEL orb tags. See http://mvel.codehaus.org) +
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/NextPage.html b/sitebricks-acceptance-tests/src/main/resources/NextPage.html new file mode 100644 index 00000000..ba5e858f --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/NextPage.html @@ -0,0 +1,52 @@ + + + + + + + + + + +
+ +
+
+

examples

+
+ + The next page in the chain received the value you typed in from the previous + page: ${persistedValue} +

+
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/PageChain.html b/sitebricks-acceptance-tests/src/main/resources/PageChain.html new file mode 100644 index 00000000..c4aa024c --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/PageChain.html @@ -0,0 +1,64 @@ + + + + + + + + + + + +
+ +
+
+

examples

+
+ + The following demonstrates page chaining. The value you type in will be + passed on to the next page on the server side, without leaking it to the + client. +

+
+ + + +
+ +


+ +
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/Repeat.html b/sitebricks-acceptance-tests/src/main/resources/Repeat.html new file mode 100644 index 00000000..95f2002b --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/Repeat.html @@ -0,0 +1,73 @@ + + + + + + + + + + + +
+ +
+
+

examples

+
+ + The following expression is repeated over every item in the bound collection of names. + +
    + @Repeat(items=names, var="name", pageVar="page") +
  • ${index}: ${name} (last? ${isLast})
  • +
+ +


+
+ + +
+ + Repeat inside a repeat: + +
    + @Repeat(items=movies, var="movie") +
  • @Repeat(items=movie.actors, var="actor")${actor}
  • +
+ +


+
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/SelectRouting.html b/sitebricks-acceptance-tests/src/main/resources/SelectRouting.html new file mode 100644 index 00000000..2ba053ec --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/SelectRouting.html @@ -0,0 +1,228 @@ + + + + + + + + + + +
+ +
+
+

examples

+ + @Repeat(items=data, var="event") +
${event}
+ + The following demonstrates a routing mechanism using the Select annotation +

+ +

GET

+
+ +
+ +
+ + +
+ +
+ + +
+ + +
+ + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ + +

POST

+
+ +
+ +
+ + +
+ +
+ + +
+ + +
+ + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ + +

PUT

+
+ + +
+ +
+ + + +
+ +
+ + + +
+ + +
+ + + + +
+ +
+ + + +
+ +
+ + + + +
+ +
+ + + +
+ + +

DELETE

+
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + + +
+ +
+ + + +
+ +
+ + + + +
+ +
+ + + +
+ + +
+ Hint: POST button removes hidden field via javascript and then submits normally via POST +
+ Bound: +

+

+
+ + +
+ +
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/ShowIf.html b/sitebricks-acceptance-tests/src/main/resources/ShowIf.html new file mode 100644 index 00000000..f6a929a5 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/ShowIf.html @@ -0,0 +1,62 @@ + + + + + + + + + + + +
+ +
+
+

examples

+
+ + The following span tag will only be shown if query parameter "show" is true. + + @ShowIf(show) + + Message : ${message} + +


+

+ toggle +

+
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/main/resources/TestPage.html b/sitebricks-acceptance-tests/src/main/resources/TestPage.html new file mode 100644 index 00000000..9e83a954 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/TestPage.html @@ -0,0 +1,8 @@ + + + show/hide + + @ShowIf(appear) +

${message} from warp-widgets!

+ + diff --git a/sitebricks-acceptance-tests/src/main/resources/WEB-INF/web.xml b/sitebricks-acceptance-tests/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..16759ef1 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,22 @@ + + + + + + webFilter + com.google.inject.servlet.GuiceFilter + + + + webFilter + /* + + + + com.google.sitebricks.example.SitebricksConfig + + + diff --git a/sitebricks-acceptance-tests/src/main/resources/default.css b/sitebricks-acceptance-tests/src/main/resources/default.css new file mode 100644 index 00000000..f4543962 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/default.css @@ -0,0 +1,339 @@ +/* +Design by Free CSS Templates +http://www.freecsstemplates.org +Released for free under a Creative Commons Attribution 2.5 License +*/ + +body { + margin: 100px 0 0 0; + padding: 0; + background: #FFFFFF url(images/img01.gif) repeat-x; + font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; + font-size: 13px; + color: #333333; +} + +h1, h2, h3 { + margin: 0; + text-transform: lowercase; + font-weight: normal; + color: #3E3E3E; +} + +h1 { + letter-spacing: -1px; + font-size: 32px; +} + +h2 { + font-size: 23px; +} + +p, ul, ol { + margin: 0 0 2em 0; + text-align: justify; + line-height: 26px; + font-size: 11px; +} + +a:link { + color: #7BAA0F; +} + +a:hover, a:active { + text-decoration: none; + color: #003448; +} + +a:visited { + color: #333333; +} + +img { + border: none; +} + +img.left { + float: left; + margin-right: 15px; +} + +img.right { + float: right; + margin-left: 15px; +} + +/* Form */ + +form { + margin: 0; + padding: 0; +} + +fieldset { + margin: 0; + padding: 0; + border: none; +} + +legend { + display: none; +} + +input, textarea, select { + font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; + font-size: 13px; + color: #333333; +} + +/* Header */ + +#header { + width: 850px; + height: 82px; + margin: 0 auto 40px auto; + background: url(images/img03.gif) repeat-x left bottom; +} + +#logo { + float: left; +} + +#logo h1 { + font-size: 38px; + color: #494949; +} + +#logo h1 sup { + vertical-align: text-top; + font-size: 24px; +} + +#logo h1 a { + color: #494949; +} + +#logo h2 { + margin-top: -10px; + font-size: 12px; + color: #A0A0A0; +} + +#logo a { + text-decoration: none; +} + +/* Menu */ + +#menu { + float: right; +} + +#menu ul { + margin: 0; + padding: 15px 0 0 0; + list-style: none; +} + +#menu li { + display: inline; +} + +#menu a { + display: block; + float: left; + background: #F5F5F5 url(images/img02.gif) repeat-x left bottom; + margin-left: 5px; + padding: 7px 20px; + text-decoration: none; + font-size: 13px; + color: #000000; +} + +#menu a:hover { + text-decoration: underline; +} + +#menu .active a { +} + +/* Page */ + +#page { + width: 850px; + margin: 0 auto; +} + +/* Content */ + +#content { + float: left; + width: 575px; +} + +/* Post */ + +.post { +} + +.post .title { + margin-bottom: 20px; + padding-bottom: 5px; + background: url(images/img11.gif) no-repeat right 50%; + border-bottom: 1px dotted #D1D1D1; +} + +.post .entry { +} + +.post .entry li { + color: #7BAA0F; +} + +.post .meta { + padding: 15px 0 60px 0; + background: url(images/img03.gif) repeat-x; +} + +.post .meta p { + margin: 0; + line-height: normal; + color: #999999; +} + +.post .meta .byline { + float: left; +} + +.post .meta .links { + float: right; +} + +.post .meta .more { + padding: 0 20px 0 18px; + background: url(images/img06.gif) no-repeat left center; +} + +.comments { + padding-left: 22px; + background: url(images/img07.gif) no-repeat left center; +} + +.post .meta .comments { + padding-left: 22px; + background: url(images/img07.gif) no-repeat left center; +} + +.post .meta b { + display: none; +} + +/* Sidebar */ + +#sidebar { + float: right; + width: 195px; +} + +#sidebar ul { + margin: 0; + padding: 0; + list-style: none; +} + +#sidebar li { + margin-bottom: 40px; +} + +#sidebar li ul { +} + +#sidebar li li { + margin: 0; + padding-left: 12px; + background: url(images/img12.gif) no-repeat left 50%; +} + +#sidebar h2 { + margin-bottom: 10px; + background: url(images/img11.gif) no-repeat right 50%; + border-bottom: 1px dotted #D1D1D1; + font-size: 16px; +} + +/* Search */ + +#search { +} + +#search h2 { + margin-bottom: 20px; +} + +#s { + width: 120px; + margin-right: 5px; + padding: 3px; + border: 1px solid #F0F0F0; +} + +#x { + padding: 3px; + background: #ECECEC url(images/img08.gif) repeat-x left bottom; + border: none; + text-transform: lowercase; + font-size: 11px; + color: #4F4F4F; +} + +/* Boxes */ + +.box1 { + padding: 20px; + background: url(images/img05.gif) no-repeat; +} + +.box2 { + color: #BABABA; +} + +.box2 h2 { + margin-bottom: 15px; + background: url(images/img10.gif) repeat-x left bottom; + font-size: 16px; + color: #FFFFFF; +} + +.box2 ul { + margin: 0; + padding: 0; + list-style: none; +} + +.box2 a:link, .box2 a:hover, .box2 a:active, .box2 a:visited { + color: #EDEDED; +} + +/* Footer */ + +#footer { + height: 400px; + min-height: 74px; + padding: 10px 0 0 0; + background: #003448 url(images/img09.gif) repeat-x; +} + +html>body #footer { + height: auto; +} + +#legal { + clear: both; + padding-top: 20px; + text-align: center; + color: #8ADE3C; +} + +#legal a { + color: #76D424; +} diff --git a/sitebricks-acceptance-tests/src/main/resources/dynamic.js b/sitebricks-acceptance-tests/src/main/resources/dynamic.js new file mode 100644 index 00000000..f437cfaf --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/dynamic.js @@ -0,0 +1,3 @@ +function hello() { + alert("${message}"); +} diff --git a/sitebricks-acceptance-tests/src/main/resources/index.html b/sitebricks-acceptance-tests/src/main/resources/index.html new file mode 100644 index 00000000..9d6789be --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/index.html @@ -0,0 +1,69 @@ + + + + + + + + + + + +
+ +
+
+

examples

+
+ + +
+ + +
+ +
+
+
+
+ + + diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/ServletContainerIntegrationTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/ServletContainerIntegrationTest.java new file mode 100644 index 00000000..c68be2bb --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/ServletContainerIntegrationTest.java @@ -0,0 +1,48 @@ +package com.google.sitebricks; + +import org.mortbay.jetty.Handler; +import org.mortbay.jetty.Server; +import org.mortbay.jetty.servlet.FilterHolder; +import org.mortbay.jetty.servlet.ServletHandler; + +import javax.servlet.*; +import java.io.IOException; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class ServletContainerIntegrationTest { + +// @NOTaTest + public final void fireUp() throws Exception { + + final Server server = new Server(8085); + final ServletHandler servletHandler = new ServletHandler(); + servletHandler.addFilterWithMapping(new FilterHolder(new Filter() { + public void init(FilterConfig filterConfig) throws ServletException { + System.out.println("*************************************************"); + final Set resourcePaths = filterConfig.getServletContext().getResourcePaths("/WEB-INF/classes"); + + + System.out.println(resourcePaths); + + System.out.println("*************************************************"); + } + + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + System.out.println("*************************************************"); + System.out.println("Hello!"); + System.out.println("*************************************************"); + } + + public void destroy() { + } + }), "/*", Handler.REQUEST); + + server.addHandler(servletHandler); + + server.start(); + server.join(); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/CaseAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/CaseAcceptanceTest.java new file mode 100644 index 00000000..9b05002d --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/CaseAcceptanceTest.java @@ -0,0 +1,19 @@ +package com.google.sitebricks.acceptance; + +import com.google.sitebricks.acceptance.page.CasePage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +/** + * @author Tom Wilson (tom@tomwilson.name) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class CaseAcceptanceTest { + public void shouldDisplayGreenFromCaseStatement() { + WebDriver driver = AcceptanceTest.createWebDriver(); + CasePage page = CasePage.open(driver); + + assert "Green".equals(page.getDisplayedColor()) : "expected color wasn't displayed"; + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/ConnegAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/ConnegAcceptanceTest.java new file mode 100644 index 00000000..0ec2933f --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/ConnegAcceptanceTest.java @@ -0,0 +1,41 @@ +package com.google.sitebricks.acceptance; + +import com.google.common.collect.ImmutableMap; +import com.google.sitebricks.acceptance.page.ConnegPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class ConnegAcceptanceTest { + + public void shouldReturnGifByDefault() { + ConnegPage page = ConnegPage.openWithHeaders(ImmutableMap.of()); + + assert page.hasContent("GIF") + : "Did not have gif!"; + } + + public void shouldReturnJpeg() { + ConnegPage page = ConnegPage.openWithHeaders(ImmutableMap.of("Accept", "image/jpeg")); + + assert page.hasContent("JPEG") + : "Did not have jpeg!"; + } + + public void shouldReturnPng() { + ConnegPage page = ConnegPage.openWithHeaders(ImmutableMap.of("Accept", "image/png")); + + assert page.hasContent("PNG") + : "Did not have jpeg!"; + } + + public void shouldReturnPngOrJpeg() { + ConnegPage page = ConnegPage.openWithHeaders(ImmutableMap.of("Accept", "image/png, image/jpeg")); + + assert page.hasContent("PNG") || page.hasContent("JPEG") + : "Did not have jpeg!"; + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/ConversionAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/ConversionAcceptanceTest.java new file mode 100644 index 00000000..dc8546da --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/ConversionAcceptanceTest.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.acceptance; + +import java.util.Calendar; +import java.util.Date; + +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +import com.google.sitebricks.acceptance.page.ConversionPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.example.SitebricksConfig; + +@Test(suiteName = AcceptanceTest.SUITE) +public class ConversionAcceptanceTest { + + public void hasConvertedTypes() { + String inboundDateFormat = SitebricksConfig.DEFAULT_DATE_TIME_FORMAT; + Date date = new Date(); + Calendar calendar = Calendar.getInstance(); + String msg = "This is a test msg"; + Double dbl = 2.2; + + WebDriver driver = AcceptanceTest.createWebDriver(); + ConversionPage page = ConversionPage.open(driver, date, calendar, inboundDateFormat, msg, dbl); + + assert page.hasCalendar(calendar) : "Calendar not bound correctly"; + assert page.hasDate(date) : "Date not bound correctly"; + assert page.hasDouble(dbl) : "Double nto bound correctly"; + assert page.hasMessage(msg) : "String not bound correctly"; + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/DecoratorAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/DecoratorAcceptanceTest.java new file mode 100644 index 00000000..7448a0d4 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/DecoratorAcceptanceTest.java @@ -0,0 +1,25 @@ +package com.google.sitebricks.acceptance; + +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +import com.google.sitebricks.acceptance.page.DecoratorPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class DecoratorAcceptanceTest { + + public void shouldRenderHelloWorldEmbeddedWithRequires() { + WebDriver driver = AcceptanceTest.createWebDriver(); + DecoratorPage page = DecoratorPage.open(driver); + + assert page.hasBasePageText(); + assert page.hasSubclassVariable(); + assert page.hasSubclassText(); + assert page.hasSubclassVariableInTemplate(); + assert page.hasBasePageVariable(); + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/DynamicJsAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/DynamicJsAcceptanceTest.java new file mode 100644 index 00000000..d9393ffb --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/DynamicJsAcceptanceTest.java @@ -0,0 +1,20 @@ +package com.google.sitebricks.acceptance; + +import com.google.sitebricks.acceptance.page.DynamicJsPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class DynamicJsAcceptanceTest { + + public void shouldRenderDynamicTextFromJsTemplate() { + WebDriver driver = AcceptanceTest.createWebDriver(); + DynamicJsPage page = DynamicJsPage.open(driver); + + assert page.hasDynamicText() : "Did not generate dynamic text from warp-widget"; + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/EmbedAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/EmbedAcceptanceTest.java new file mode 100644 index 00000000..538e9b0a --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/EmbedAcceptanceTest.java @@ -0,0 +1,21 @@ +package com.google.sitebricks.acceptance; + +import com.google.sitebricks.acceptance.page.EmbedPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class EmbedAcceptanceTest { + + public void shouldRenderHelloWorldEmbeddedWithRequires() { + WebDriver driver = AcceptanceTest.createWebDriver(); + EmbedPage page = EmbedPage.open(driver); + + assert page.hasCssLink() : "Did not contain require widget (css link) from embedded page"; + assert page.hasHelloWorldMessage() : "Did not contain hellow world message"; + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/FormsAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/FormsAcceptanceTest.java new file mode 100644 index 00000000..f9742af6 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/FormsAcceptanceTest.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.acceptance; + +import com.google.sitebricks.acceptance.page.FormsPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +import java.util.Date; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class FormsAcceptanceTest { + private static final String SOME_TEXT = "aoskdopaksdoaskd" + new Date(); + + public void shouldRenderDynamicTextFromTextFieldBinding() { + WebDriver driver = AcceptanceTest.createWebDriver(); + FormsPage page = FormsPage.open(driver); + + final String boundAutobots = "Optimus, Rodimus, UltraMagnus"; + final String[] strings = boundAutobots.split(", "); + + page.enterText(SOME_TEXT); + page.enterAutobots(strings[0], strings[1], strings[2]); + page.send(); + + assert page.hasBoundText(SOME_TEXT) : "Did not generate dynamic text from form binding"; + assert page.hasBoundAutobots(boundAutobots) : "Did not generate text from list binding"; + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/HelloWorldAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/HelloWorldAcceptanceTest.java new file mode 100644 index 00000000..6263e8bd --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/HelloWorldAcceptanceTest.java @@ -0,0 +1,41 @@ +package com.google.sitebricks.acceptance; + +import com.google.sitebricks.acceptance.page.HelloWorldPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class HelloWorldAcceptanceTest { + + public void shouldRenderDynamicTextFromHelloWorld() { + WebDriver driver = AcceptanceTest.createWebDriver(); + HelloWorldPage page = HelloWorldPage.open(driver, "/hello"); + + assertHelloWorldContent(page); + } + + public void shouldRenderDynamicTextFromHelloWorldService() { + WebDriver driver = AcceptanceTest.createWebDriver(); + HelloWorldPage page = HelloWorldPage.open(driver, "/helloservice"); + + assertHelloWorldContent(page); + } + + public void shouldRenderDynamicTextFromHelloWorldServiceDirect() { + WebDriver driver = AcceptanceTest.createWebDriver(); + HelloWorldPage page = HelloWorldPage.open(driver, "/helloservice/direct"); + + assertHelloWorldContent(page); + } + + private void assertHelloWorldContent(HelloWorldPage page) { + assert page.hasHelloWorldMessage() : "Did not generate dynamic text from el expression"; + assert page.hasCorrectDoctype() : "Did not contain the expected doctype declaration at the start of the HTML file"; + assert page.hasMangledString() : "Did not contain method-generated string"; + assert page.hasNonSelfClosingScriptTag() : "Did not contain proper script tag with closing tag"; + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/HiddenFieldMethodAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/HiddenFieldMethodAcceptanceTest.java new file mode 100644 index 00000000..f9fb8258 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/HiddenFieldMethodAcceptanceTest.java @@ -0,0 +1,25 @@ +package com.google.sitebricks.acceptance; + +import com.google.sitebricks.acceptance.page.HiddenFieldMethodPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +/** + * @author Peter Knego + */ + +@Test(suiteName = AcceptanceTest.SUITE) +public class HiddenFieldMethodAcceptanceTest { + + public void shouldRenderDynamicTextFromTextFieldBinding() { + WebDriver driver = AcceptanceTest.createWebDriver(); + HiddenFieldMethodPage page = HiddenFieldMethodPage.open(driver); + + page.enterText("just some text"); + page.submitPut(); + + // was the message generated via PUT method? + assert page.isPutMessage(); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/I18nAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/I18nAcceptanceTest.java new file mode 100644 index 00000000..733c74a8 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/I18nAcceptanceTest.java @@ -0,0 +1,25 @@ +package com.google.sitebricks.acceptance; + +import com.google.common.collect.ImmutableMap; +import com.google.sitebricks.acceptance.page.I18nPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class I18nAcceptanceTest { + + public void shouldRenderLocalizedDynamicTextEnUs() { + I18nPage page = I18nPage.openWithHeaders(ImmutableMap.of("Accept-Language", "en-US")); + + assert page.hasHelloTo(I18nPage.NAME) : "Did not generate dynamic text from i18n message set"; + } + + public void shouldRenderLocalizedDynamicTextFrCa() { + I18nPage page = I18nPage.openWithHeaders(ImmutableMap.of("Accept-Language", "fr-CA")); + + assert page.hasBonjourTo(I18nPage.NAME) : "Did not generate dynamic text from i18n message set"; + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/JettyAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/JettyAcceptanceTest.java new file mode 100644 index 00000000..382db02b --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/JettyAcceptanceTest.java @@ -0,0 +1,40 @@ +package com.google.sitebricks.acceptance; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.acceptance.util.Jetty; +import org.testng.annotations.AfterSuite; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Test; + +import java.io.File; + +/** + * @author Tom Wilson (tom@tomwilson.name) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class JettyAcceptanceTest { + private static final String BUILDR_RESOURCE_DIR = "acceptance-test/src/main/resources"; + private static final String STD_RESOURCE_DIR = "src/main/resources"; + + private Jetty server; + + public JettyAcceptanceTest() { + File standardDir = new File(STD_RESOURCE_DIR); + + if (standardDir.exists()) { + server = new Jetty(STD_RESOURCE_DIR); + } else { + server = new Jetty(BUILDR_RESOURCE_DIR); + } + } + + @BeforeSuite + public void start() throws Exception { + server.start(); + } + + @AfterSuite + public void stop() throws Exception { + server.stop(); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PageChainingAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PageChainingAcceptanceTest.java new file mode 100644 index 00000000..02bbe7f9 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PageChainingAcceptanceTest.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.acceptance; + +import com.google.sitebricks.acceptance.page.PageChainPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +import java.util.Date; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class PageChainingAcceptanceTest { + private static final String SOME_TEXT = "some random textina" + new Date(); + + public void shouldPassEnteredTextToNextPage() { + WebDriver driver = AcceptanceTest.createWebDriver(); + PageChainPage page = PageChainPage.open(driver); + + page.enterText(SOME_TEXT); + page.next(); + + // We should now be on the NextPage page + assert driver.findElement(By.xpath("//div[@class='entry']")) + .getText() + .contains(SOME_TEXT) + : "Value did not get passed via page chaining to next page"; + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PostableRestfuWebServiceAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PostableRestfuWebServiceAcceptanceTest.java new file mode 100644 index 00000000..3ea67866 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/PostableRestfuWebServiceAcceptanceTest.java @@ -0,0 +1,49 @@ +package com.google.sitebricks.acceptance; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.client.Web; +import com.google.sitebricks.client.WebResponse; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.conversion.Converter; +import com.google.sitebricks.conversion.ConverterRegistry; +import com.google.sitebricks.conversion.StandardTypeConverter; +import com.google.sitebricks.example.RestfulWebService; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class PostableRestfuWebServiceAcceptanceTest { + + public void shouldTransportJsonWithoutTemplate() { + RestfulWebService.Book perdido = new RestfulWebService.Book(); + perdido.setAuthor(RestfulWebService.CHINA_MIEVILLE); + perdido.setName(RestfulWebService.PERDIDO_STREET_STATION); + perdido.setPageCount(RestfulWebService.PAGE_COUNT); + + + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/postable?p1=v1,v2") + .transports(RestfulWebService.Book.class) + .over(Json.class) + .post(perdido); + + // Should ping us the author back. + assert response.toString().contains(perdido.getAuthor()) : response.toString(); + } + + private Injector createInjector() { + return Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(ConverterRegistry.class).toInstance(new StandardTypeConverter( + ImmutableSet.of())); + } + }); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RepeatAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RepeatAcceptanceTest.java new file mode 100644 index 00000000..1deff94e --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RepeatAcceptanceTest.java @@ -0,0 +1,40 @@ +package com.google.sitebricks.acceptance; + +import com.google.sitebricks.acceptance.page.RepeatPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.apache.commons.collections.CollectionUtils; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Tom Wilson (tom@tomwilson.name) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class RepeatAcceptanceTest { + + public void shouldRepeatItemsFromCollection() { + WebDriver driver = AcceptanceTest.createWebDriver(); + RepeatPage page = RepeatPage.open(driver); + + List expectedNames = Arrays.asList( + "0: Dhanji (last? false)", + "1: Josh (last? false)", + "2: Jody (last? false)", + "3: Iron Man (last? true)"); + List expectedMovies = + Arrays.asList("Dhanji Josh Jody Iron Man", + "Dhanji Josh Jody Iron Man", + "Dhanji Josh Jody Iron Man"); + + List actualNames = page.getRepeatedNames(); + List actualMovies = page.getRepeatedMovies(); + + assert CollectionUtils.isEqualCollection(expectedNames, actualNames) + : "repeated names didn't match what was expected"; + assert CollectionUtils.isEqualCollection(expectedMovies, actualMovies) + : "repeated movies didn't match what was expected"; + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceAcceptanceTest.java new file mode 100644 index 00000000..fb360358 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceAcceptanceTest.java @@ -0,0 +1,99 @@ +package com.google.sitebricks.acceptance; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.client.Web; +import com.google.sitebricks.client.WebResponse; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.client.transport.Text; +import com.google.sitebricks.conversion.Converter; +import com.google.sitebricks.conversion.ConverterRegistry; +import com.google.sitebricks.conversion.StandardTypeConverter; +import com.google.sitebricks.example.RestfulWebService; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletResponse; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class RestfuWebServiceAcceptanceTest { + + public void shouldTransportJsonWithoutTemplate() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/service") + .transports(String.class) + .over(Json.class) + .get(); + + assertBookResponse(response); + } + + + private Injector createInjector() { + return Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(ConverterRegistry.class).toInstance(new StandardTypeConverter( + ImmutableSet.of())); + } + }); + } + + public void shouldRedirect() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/service") + .transports(String.class) + .over(Text.class) + .post(""); + + assertRedirectResponse(response); + } + + public void shouldTransportJsonWithoutTemplateNoAnnotations() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/no_annotations/service") + .transports(String.class) + .over(Json.class) + .get(); + + assertBookResponse(response); + } + + public void shouldRedirectNoAnnotations() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/no_annotations/service") + .transports(String.class) + .over(Text.class) + .post(""); + + assertRedirectResponse(response); + } + + private static void assertRedirectResponse(WebResponse response) { + assert HttpServletResponse.SC_MOVED_TEMPORARILY == response.status() : response.toString(); + assert response.getHeaders().get("Location").endsWith("/other"); + } + + private static void assertBookResponse(WebResponse response) { + assert HttpServletResponse.SC_OK == response.status(); + + // Make sure the headers were set. + assert response.getHeaders().containsKey("hi"); + assert "there".equals(response.getHeaders().get("hi")); + assert response.getHeaders().containsKey("Content-Type"); + + // assert stuff about the content itself. + RestfulWebService.Book book = response.to(RestfulWebService.Book.class).using(Json.class); + assert RestfulWebService.CHINA_MIEVILLE.equals(book.getAuthor()); + assert RestfulWebService.PERDIDO_STREET_STATION.equals(book.getName()); + assert RestfulWebService.PAGE_COUNT == book.getPageCount(); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithCRUDAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithCRUDAcceptanceTest.java new file mode 100644 index 00000000..5ecab23f --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithCRUDAcceptanceTest.java @@ -0,0 +1,104 @@ +package com.google.sitebricks.acceptance; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.client.Web; +import com.google.sitebricks.client.WebResponse; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.conversion.Converter; +import com.google.sitebricks.conversion.ConverterRegistry; +import com.google.sitebricks.conversion.StandardTypeConverter; + +import org.testng.annotations.Test; + +import static com.google.sitebricks.example.RestfulWebServiceWithCRUD.BASE_SERVICE_PATH; +import static com.google.sitebricks.example.RestfulWebServiceWithCRUD.CREATE; +import static com.google.sitebricks.example.RestfulWebServiceWithCRUD.DELETE; +import static com.google.sitebricks.example.RestfulWebServiceWithCRUD.READ_COLLECTION; +import static com.google.sitebricks.example.RestfulWebServiceWithCRUD.READ_INDIVIDUAL; +import static com.google.sitebricks.example.RestfulWebServiceWithCRUD.UPDATE; + +/** + * @author Jason van Zyl + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class RestfuWebServiceWithCRUDAcceptanceTest { + + public void create() { + String url = AcceptanceTest.BASE_URL + BASE_SERVICE_PATH; + System.out.println("POST " + url); + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(url) + .transports(String.class) + .over(Json.class) + .post(""); + + assert CREATE.equals(response.toString()) : response.toString(); + } + + public void readCollection() { + String url = AcceptanceTest.BASE_URL + BASE_SERVICE_PATH; + System.out.println("GET " + url); + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(url) + .transports(String.class) + .over(Json.class) + .get(); + + assert READ_COLLECTION.equals(response.toString()); + } + + public void readIndividual() { + String url = AcceptanceTest.BASE_URL + BASE_SERVICE_PATH + "/1"; + System.out.println("GET " + url); + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(url) + .transports(String.class) + .over(Json.class) + .get(); + + assert READ_INDIVIDUAL.equals(response.toString()) : response.toString(); + } + + public void update() { + String url = AcceptanceTest.BASE_URL + BASE_SERVICE_PATH + "/1"; + System.out.println("PUT " + url); + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(url) + .transports(String.class) + .over(Json.class) + .put(""); + + assert UPDATE.equals(response.toString()); + } + + public void delete() { + String url = AcceptanceTest.BASE_URL + BASE_SERVICE_PATH + "/1"; + System.out.println("DELETE " + url); + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(url) + .transports(String.class) + .over(Json.class) + .delete(); + + assert DELETE.equals(response.toString()); + } + + + private Injector createInjector() { + return Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(ConverterRegistry.class).toInstance(new StandardTypeConverter( + ImmutableSet.of())); + } + }); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithCRUDConversionsAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithCRUDConversionsAcceptanceTest.java new file mode 100644 index 00000000..6e591c56 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithCRUDConversionsAcceptanceTest.java @@ -0,0 +1,111 @@ +package com.google.sitebricks.acceptance; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.client.Web; +import com.google.sitebricks.client.WebResponse; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.conversion.Converter; +import com.google.sitebricks.conversion.ConverterRegistry; +import com.google.sitebricks.conversion.StandardTypeConverter; +import com.google.sitebricks.example.RestfulWebServiceWithCRUDConversions; +import com.google.sitebricks.example.RestfulWebServiceWithCRUDConversions.Widget; +import org.testng.annotations.Test; + +import java.util.Date; +import java.util.List; + +@Test(suiteName = AcceptanceTest.SUITE) +public class RestfuWebServiceWithCRUDConversionsAcceptanceTest { + private Widget testWidget = new Widget(100, "Widget 100", new Date(), 1.50); + private Widget widgetOne = RestfulWebServiceWithCRUDConversions.findWidget(1).clone(); + + public void create() { + String url = AcceptanceTest.BASE_URL + RestfulWebServiceWithCRUDConversions.AT_ME; + System.out.println("POST " + url); + + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(url) + .transports(Widget.class) + .over(Json.class) + .post(testWidget); + + Widget result = response.to(Widget.class).using(Json.class); + assert result.equals(testWidget); + } + + public void readCollection() { + String url = AcceptanceTest.BASE_URL + RestfulWebServiceWithCRUDConversions.AT_ME; + System.out.println("GET " + url); + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(url) + .transports(String.class) + .over(Json.class).get(); + + @SuppressWarnings("unchecked") + List result = response.to(List.class).using(Json.class); + + assert result.size() == RestfulWebServiceWithCRUDConversions.widgets.size(); + } + + private Injector createInjector() { + return Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(ConverterRegistry.class).toInstance(new StandardTypeConverter( + ImmutableSet.of())); + } + }); + } + + public void readIndividual() { + String url = AcceptanceTest.BASE_URL + RestfulWebServiceWithCRUDConversions.AT_ME + "/" + widgetOne.getId(); + System.out.println("GET " + url); + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(url) + .transports(String.class) + .over(Json.class) + .get(); + + Widget result = response.to(Widget.class).using(Json.class); + assert result.equals(widgetOne); + } + + public void update() { + String url = AcceptanceTest.BASE_URL + RestfulWebServiceWithCRUDConversions.AT_ME; + + widgetOne.setPrice(5.50); + System.out.println("PUT " + url); + + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(url) + .transports(Widget.class) + .over(Json.class) + .put(widgetOne); + + Widget result = response.to(Widget.class).using(Json.class); + assert result.equals(widgetOne); + } + + + public void delete() { + String url = AcceptanceTest.BASE_URL + RestfulWebServiceWithCRUDConversions.AT_ME + "/" + testWidget.getId(); + System.out.println("DELETE " + url); + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(url) + .transports(String.class) + .over(Json.class) + .delete(); + + Widget result = response.to(Widget.class).using(Json.class); + assert result.equals(testWidget); + } + +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithMatrixParamsAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithMatrixParamsAcceptanceTest.java new file mode 100644 index 00000000..18097b5f --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithMatrixParamsAcceptanceTest.java @@ -0,0 +1,57 @@ +package com.google.sitebricks.acceptance; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.client.Web; +import com.google.sitebricks.client.WebResponse; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.conversion.Converter; +import com.google.sitebricks.conversion.ConverterRegistry; +import com.google.sitebricks.conversion.StandardTypeConverter; +import com.google.sitebricks.example.RestfulWebServiceWithMatrixParams; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class RestfuWebServiceWithMatrixParamsAcceptanceTest { + + public void shouldServiceTopLevelPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/matrixpath") + .transports(String.class) + .over(Json.class) + .get(); + + assert RestfulWebServiceWithMatrixParams.TOPLEVEL.equals(response.toString()) + : response.toString(); + } + + public void shouldServiceVariableThreeLevelSubPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/matrixpath/val;param1=val1;param2=val2/athing") + .transports(String.class) + .over(Json.class) + .post(""); + + assert ("{param1=[val1], param2=[val2]}" // Multimap of matrix params + + "_" + "val;param1=val1;param2=val2" // Variable path fragment #1 + + "_" + "athing") // Variable path fragment #2 + .equals(response.toString()) : response.toString(); + } + + private Injector createInjector() { + return Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(ConverterRegistry.class).toInstance(new StandardTypeConverter( + ImmutableSet.of())); + } + }); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithSubpaths2AcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithSubpaths2AcceptanceTest.java new file mode 100644 index 00000000..a712ba9b --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithSubpaths2AcceptanceTest.java @@ -0,0 +1,167 @@ +package com.google.sitebricks.acceptance; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.client.Web; +import com.google.sitebricks.client.WebResponse; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.conversion.Converter; +import com.google.sitebricks.conversion.ConverterRegistry; +import com.google.sitebricks.conversion.StandardTypeConverter; +import com.google.sitebricks.example.RestfulWebServiceWithSubpaths2; +import org.testng.annotations.Test; + +import static com.google.sitebricks.example.RestfulWebServiceWithSubpaths2.TOPLEVEL; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class RestfuWebServiceWithSubpaths2AcceptanceTest { + + public void shouldServiceTopLevelDynamicPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath2/" + TOPLEVEL) + .transports(String.class) + .over(Json.class) + .get(); + + assert TOPLEVEL.equals(response.toString()); + } + + public void shouldServiceFirstLevelStaticPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath2/junk/subpath1") + .transports(String.class) + .over(Json.class) + .post(""); + + assert RestfulWebServiceWithSubpaths2.PATH_1.equals(response.toString()) : response.toString(); + } + + public void shouldServiceSameFirstLevelStaticPathWithPutMethod() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath2/junk/subpath1") + .transports(String.class) + .over(Json.class) + .put(""); + + assert RestfulWebServiceWithSubpaths2.PATH_1_PUT.equals(response.toString()) + : response.toString(); + } + + public void shouldServiceSameFirstLevelStaticPathWithDeleteMethod() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath2/junk/subpath1") + .transports(String.class) + .over(Json.class) + .delete(); + + assert RestfulWebServiceWithSubpaths2.PATH_1_DELETE.equals(response.toString()) + : response.toString(); + } + + public void shouldServiceTwoLevelDynamicPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath2/junk/more_junk") + .transports(String.class) + .over(Json.class) + .post(""); + + assert "junk_more_junk".equals(response.toString()) : response.toString(); + } + + public void shouldServiceTwoLevelDynamicPathWithDeleteMethod() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath2/junk/more_junk") + .transports(String.class) + .over(Json.class) + .delete(); + + assert "delete:junk_more_junk".equals(response.toString()) : response.toString(); + } + + public void shouldServiceThreeLevelDynamicPathWithDeleteMethod() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath2/junk/more_junk/most_junk") + .transports(String.class) + .over(Json.class) + .delete(); + + assert "delete:junk_more_junk_most_junk".equals(response.toString()) : response.toString(); + } + + public void shouldServiceThreeLevelDynamicPathWithPutMethod() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath2/junk/more_junk/most_junk") + .transports(String.class) + .over(Json.class) + .put(""); + + assert "put:junk_more_junk_most_junk".equals(response.toString()) : response.toString(); + } + + public void shouldServiceThreeLevelDynamicPathWithPostMethod() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath2/junk/more_junk/most_junk") + .transports(String.class) + .over(Json.class) + .post(""); + + assert "post:junk_more_junk_most_junk".equals(response.toString()) : response.toString(); + } + + public void shouldServiceThreeLevelDynamicPathWithGetMethod() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath2/junk/more_junk/most_junk") + .transports(String.class) + .over(Json.class) + .get(); + + assert "get:junk_more_junk_most_junk".equals(response.toString()) : response.toString(); + } + + private Injector createInjector() { + return Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(ConverterRegistry.class).toInstance(new StandardTypeConverter( + ImmutableSet.of())); + } + }); + } +// +// public void shouldService4LevelMixedPathWithGetMethod() { +// WebResponse response = createInjector() +// .getInstance(Web.class) +// .clientOf(AcceptanceTest.BASE_URL + "/superpath2/junk/more_junk/most_junk/4l") +// .transports(String.class) +// .over(Json.class) +// .get(); +// +// assert "4l:get:junk_more_junk_most_junk".equals(response.toString()) : response.toString(); +// } +// +// public void shouldService4LevelMixedPathWithPostMethod() { +// WebResponse response = createInjector() +// .getInstance(Web.class) +// .clientOf(AcceptanceTest.BASE_URL + "/superpath2/junk/more_junk/most_junk/4l") +// .transports(String.class) +// .over(Json.class) +// .post(""); +// +// assert "4l:post:junk_more_junk_most_junk".equals(response.toString()) : response.toString(); +// } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithSubpathsAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithSubpathsAcceptanceTest.java new file mode 100644 index 00000000..3752cf4a --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/RestfuWebServiceWithSubpathsAcceptanceTest.java @@ -0,0 +1,112 @@ +package com.google.sitebricks.acceptance; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.client.Web; +import com.google.sitebricks.client.WebResponse; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.conversion.Converter; +import com.google.sitebricks.conversion.ConverterRegistry; +import com.google.sitebricks.conversion.StandardTypeConverter; +import com.google.sitebricks.example.RestfulWebServiceWithSubpaths; +import org.testng.annotations.Test; + +import java.util.Date; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class RestfuWebServiceWithSubpathsAcceptanceTest { + + public void shouldServiceTopLevelPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath") + .transports(String.class) + .over(Json.class) + .get(); + + assert RestfulWebServiceWithSubpaths.TOPLEVEL.equals(response.toString()); + } + + public void shouldServiceFirstSubPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath/subpath1") + .transports(String.class) + .over(Json.class) + .post(""); + + assert RestfulWebServiceWithSubpaths.PATH_1.equals(response.toString()) : response.toString(); + } + + public void shouldServiceSecondSubPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath/subpath2") + .transports(String.class) + .over(Json.class) + .post(""); + + assert RestfulWebServiceWithSubpaths.PATH_2.equals(response.toString()) : response.toString(); + } + + public void shouldServiceThirdSubPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath/subpath3") + .transports(String.class) + .over(Json.class) + .post(""); + + assert RestfulWebServiceWithSubpaths.PATH_3.equals(response.toString()) : response.toString(); + } + + public void shouldServiceVariableTwoLevelSubPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath/subpath1/a_thing") + .transports(String.class) + .over(Json.class) + .post(""); + + assert "a_thing".equals(response.toString()) : response.toString(); + } + + public void shouldServiceVariableThreeLevelSubPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath/subpath1/a_thing/another_thing") + .transports(String.class) + .over(Json.class) + .post(""); + + assert "a_thing_another_thing".equals(response.toString()) : response.toString(); + } + + public void shouldServiceVariableTwoLevelSubPath2() { + String aString = "aoskdoaksd" + new Date().hashCode(); + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/superpath/subpath3/" + aString) + .transports(String.class) + .over(Json.class) + .post(""); + + // Should be reflected + assert aString.equals(response.toString()) : response.toString(); + } + + private Injector createInjector() { + return Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(ConverterRegistry.class).toInstance(new StandardTypeConverter( + ImmutableSet.of())); + } + }); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/SelectRoutingAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/SelectRoutingAcceptanceTest.java new file mode 100644 index 00000000..0fd3e4e4 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/SelectRoutingAcceptanceTest.java @@ -0,0 +1,254 @@ +package com.google.sitebricks.acceptance; + +import com.google.sitebricks.acceptance.page.SelectRoutingPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +@Test(suiteName = AcceptanceTest.SUITE) +public class SelectRoutingAcceptanceTest { + + public void shouldRenderDivForDefaultGetOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForDefaultGetOnly"); + assert page.hasExpectedDiv("defaultGet"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForFooGetOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForFooGetOnly"); + assert page.hasExpectedDiv("fooGet"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForBarGetOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForBarGetOnly"); + assert page.hasExpectedDiv("barGet"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForFooBarGet() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForFooBarGet"); + + assert page.hasExpectedDiv("fooGet"); + assert page.hasExpectedDiv("barGet"); + assert page.hasExpectedDivCount(2); + } + + public void shouldRenderDivForUnknownGet() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForUnknownGet"); + assert page.hasExpectedDiv("defaultGet"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForUnknownAndFooGet() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForUnknownAndFooGet"); + assert page.hasExpectedDiv("fooGet"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForRedirectGet() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForRedirectGet"); + assert page.hasExpectedDiv("defaultGet"); + assert page.hasExpectedDiv("redirectGet"); + assert page.hasExpectedDivCount(2); + } + + public void shouldRenderDivForDefaultPostOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForDefaultPostOnly"); + assert page.hasExpectedDiv("defaultPost"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForFooPostOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForFooPostOnly"); + assert page.hasExpectedDiv("fooPost"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForBarPostOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForBarPostOnly"); + assert page.hasExpectedDiv("barPost"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForFooBarPost() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForFooBarPost"); + + assert page.hasExpectedDiv("fooPost"); + assert page.hasExpectedDiv("barPost"); + assert page.hasExpectedDivCount(2); + } + + public void shouldRenderDivForUnknownPost() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForUnknownPost"); + assert page.hasExpectedDiv("defaultPost"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForUnknownAndFooPost() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForUnknownAndFooPost"); + assert page.hasExpectedDiv("fooPost"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForRedirectPost() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForRedirectPost"); + assert page.hasExpectedDiv("defaultGet"); + assert page.hasExpectedDiv("redirectPost"); + assert page.hasExpectedDivCount(2); + } + + + public void shouldRenderDivForDefaultPutOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForDefaultPutOnly"); + assert page.hasExpectedDiv("defaultPut"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForFooPutOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForFooPutOnly"); + assert page.hasExpectedDiv("fooPut"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForBarPutOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForBarPutOnly"); + assert page.hasExpectedDiv("barPut"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForFooBarPut() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForFooBarPut"); + + assert page.hasExpectedDiv("fooPut"); + assert page.hasExpectedDiv("barPut"); + assert page.hasExpectedDivCount(2); + } + + public void shouldRenderDivForUnknownPut() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForUnknownPut"); + assert page.hasExpectedDiv("defaultPut"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForUnknownAndFooPut() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForUnknownAndFooPut"); + assert page.hasExpectedDiv("fooPut"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForRedirectPut() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForRedirectPut"); + assert page.hasExpectedDiv("defaultGet"); + assert page.hasExpectedDiv("redirectPut"); + assert page.hasExpectedDivCount(2); + } + + + public void shouldRenderDivForDefaultDeleteOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForDefaultDeleteOnly"); + assert page.hasExpectedDiv("defaultDelete"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForFooDeleteOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForFooDeleteOnly"); + assert page.hasExpectedDiv("fooDelete"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForBarDeleteOnly() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForBarDeleteOnly"); + assert page.hasExpectedDiv("barDelete"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForFooBarDelete() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForFooBarDelete"); + + assert page.hasExpectedDiv("fooDelete"); + assert page.hasExpectedDiv("barDelete"); + assert page.hasExpectedDivCount(2); + } + + public void shouldRenderDivForUnknownDelete() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForUnknownDelete"); + assert page.hasExpectedDiv("defaultDelete"); + assert page.hasExpectedDivCount(1); + } + + + public void shouldRenderDivForUnknownAndFooDelete() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForUnknownAndFooDelete"); + assert page.hasExpectedDiv("fooDelete"); + assert page.hasExpectedDivCount(1); + } + + public void shouldRenderDivForRedirectDelete() { + SelectRoutingPage page = loadPage(); + + page.submit("shouldRenderDivForRedirectDelete"); + assert page.hasExpectedDiv("defaultGet"); + assert page.hasExpectedDiv("redirectDelete"); + assert page.hasExpectedDivCount(2); + } + + private SelectRoutingPage loadPage() { + WebDriver driver = AcceptanceTest.createWebDriver(); + return SelectRoutingPage.open(driver); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/SpiRestfuWebServiceWithSubpaths2AcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/SpiRestfuWebServiceWithSubpaths2AcceptanceTest.java new file mode 100644 index 00000000..b391250a --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/SpiRestfuWebServiceWithSubpaths2AcceptanceTest.java @@ -0,0 +1,52 @@ +package com.google.sitebricks.acceptance; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.client.Web; +import com.google.sitebricks.client.WebResponse; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.conversion.Converter; +import com.google.sitebricks.conversion.ConverterRegistry; +import com.google.sitebricks.conversion.StandardTypeConverter; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class SpiRestfuWebServiceWithSubpaths2AcceptanceTest { + + public void shouldServiceTopLevelDynamicPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/spi/test") + .transports(String.class) + .over(Json.class) + .get(); + + assert "get:top".equals(response.toString()); + } + + public void shouldServiceFirstLevelStaticPath() { + WebResponse response = createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/spi/test") + .transports(String.class) + .over(Json.class) + .post(""); + + assert "post:junk_subpath1".equals(response.toString()) : response.toString(); + } + + private Injector createInjector() { + return Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(ConverterRegistry.class).toInstance(new StandardTypeConverter( + ImmutableSet.of())); + } + }); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/StatsAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/StatsAcceptanceTest.java new file mode 100644 index 00000000..922b37c6 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/StatsAcceptanceTest.java @@ -0,0 +1,24 @@ +package com.google.sitebricks.acceptance; + +import com.google.sitebricks.acceptance.page.StatsPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Test(suiteName = AcceptanceTest.SUITE) +public class StatsAcceptanceTest { + + public void shouldRenderStatsForPageRequest() { + WebDriver driver = AcceptanceTest.createWebDriver(); + + // Request start page. + driver.get(AcceptanceTest.BASE_URL); + + StatsPage statsPage = StatsPage.open(driver); + + assert statsPage.hasNonZeroStats() : "Recorded stats should be at least 1"; + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/CasePage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/CasePage.java new file mode 100644 index 00000000..b1885f77 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/CasePage.java @@ -0,0 +1,29 @@ +package com.google.sitebricks.acceptance.page; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.support.How; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +public class CasePage { + + @FindBy(how = How.XPATH, using = "//div[@class='entry']/font[@color='green']") + private WebElement entry; + + private WebDriver driver; + + public CasePage(WebDriver driver) { + this.driver = driver; + } + + public String getDisplayedColor() { + return entry.getText(); + } + + public static CasePage open(WebDriver driver) { + driver.get(AcceptanceTest.BASE_URL + "/case"); + return PageFactory.initElements(driver, CasePage.class); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/ConnegPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/ConnegPage.java new file mode 100644 index 00000000..8e9ade95 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/ConnegPage.java @@ -0,0 +1,45 @@ +package com.google.sitebricks.acceptance.page; + +import com.google.inject.Guice; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.client.Web; +import com.google.sitebricks.client.transport.Text; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; + +import java.util.Map; + +/** + * Page object that wraps the content negotiation page. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class ConnegPage { + private String content; + + private ConnegPage(String content) { + try { + this.content = DocumentHelper.parseText(content) + .selectSingleNode("//div['content'][1]") + .getText(); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + } + + public boolean hasContent(String content) { + return this.content.trim().equals(content); + } + + public static ConnegPage openWithHeaders(Map headers) { + String content = Guice.createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/conneg", headers) + .transports(String.class) + .over(Text.class) + .get() + .toString(); + + return new ConnegPage(content); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/ConversionPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/ConversionPage.java new file mode 100644 index 00000000..5c314041 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/ConversionPage.java @@ -0,0 +1,95 @@ +package com.google.sitebricks.acceptance.page; + + +import java.net.URLEncoder; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.PageFactory; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.example.SitebricksConfig; + +public class ConversionPage { + private WebDriver driver; + + public ConversionPage(WebDriver driver) { + this.driver = driver; + } + + public boolean hasDate(Date date) { + SimpleDateFormat sdf = new SimpleDateFormat(SitebricksConfig.DEFAULT_DATE_TIME_FORMAT); + String target = sdf.format(date); + + return driver.findElement(By.id("boundDate")) + .getText() + .contains(target); + } + + public boolean hasCalendar(Calendar calendar) { + SimpleDateFormat sdf = new SimpleDateFormat(SitebricksConfig.DEFAULT_DATE_TIME_FORMAT); + String target = sdf.format(calendar.getTime()); + + String node = driver.getPageSource(); + return driver.findElement(By.id("boundCalendar")) + .getText() + .contains(target); + } + + public boolean hasMessage(String message) { + return driver.findElement(By.id("boundText")) + .getText() + .contains(message); + } + + public boolean hasDouble(Double dbl) { + return driver.findElement(By.id("boundDouble")) + .getText() + .contains(dbl.toString()); + } + + public static ConversionPage open(WebDriver driver, Date date, Calendar calendar, String dateFormat, String msg, Double dbl) { + SimpleDateFormat sdf = new SimpleDateFormat(dateFormat); + StringBuilder sb = new StringBuilder (); + + if (date != null) { + if (sb.length() > 0) + sb.append("&"); + sb.append ("date=").append(encode(sdf.format(date))); + } + + if (calendar != null) { + if (sb.length() > 0) + sb.append("&"); + sb.append ("calendar=").append(encode(sdf.format(calendar.getTime()))); + } + + if (msg != null) { + if (sb.length() > 0) + sb.append("&"); + sb.append ("message=").append(encode(msg)); + } + + if (msg != null) { + if (sb.length() > 0) + sb.append("&"); + sb.append ("dbl=").append(encode(dbl.toString())); + } + + sb.insert(0,"/conversion?").insert(0, AcceptanceTest.BASE_URL); + driver.get(sb.toString()); + return PageFactory.initElements(driver, ConversionPage.class); + } + + private static String encode(String s){ + try { + return URLEncoder.encode(s,"UTF-8"); + } + catch(Exception e){ + return s; + } + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/DecoratorPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/DecoratorPage.java new file mode 100644 index 00000000..723e3501 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/DecoratorPage.java @@ -0,0 +1,36 @@ +package com.google.sitebricks.acceptance.page; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.PageFactory; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; + +public class DecoratorPage { + + private WebDriver driver; + + public DecoratorPage(WebDriver driver) { + this.driver = driver; + } + public boolean hasBasePageText() { + return driver.getPageSource().contains("Text defined in"); + } + public boolean hasBasePageVariable() { + return driver.getPageSource().contains("from the superclass"); + } + public boolean hasSubclassVariableInTemplate() { + return driver.getPageSource().contains("This comes from the subclass"); + } + public boolean hasSubclassVariable() { + return driver.getPageSource().contains("very cool"); + } + + public boolean hasSubclassText() { + return driver.getPageSource().contains("This is in the extension"); + } + + public static DecoratorPage open(WebDriver driver) { + driver.get(AcceptanceTest.BASE_URL + "/template"); + return PageFactory.initElements(driver, DecoratorPage.class); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/DynamicJsPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/DynamicJsPage.java new file mode 100644 index 00000000..e1b7d70b --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/DynamicJsPage.java @@ -0,0 +1,24 @@ +package com.google.sitebricks.acceptance.page; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.example.DynamicJs; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.PageFactory; + +public class DynamicJsPage { + + private WebDriver driver; + + public DynamicJsPage(WebDriver driver) { + this.driver = driver; + } + + public boolean hasDynamicText() { + return driver.getPageSource().contains(DynamicJs.A_MESSAGE); + } + + public static DynamicJsPage open(WebDriver driver) { + driver.get(AcceptanceTest.BASE_URL + "/dynamic.js"); + return PageFactory.initElements(driver, DynamicJsPage.class); + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/EmbedPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/EmbedPage.java new file mode 100644 index 00000000..f4a53cc9 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/EmbedPage.java @@ -0,0 +1,32 @@ +package com.google.sitebricks.acceptance.page; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.PageFactory; + +public class EmbedPage { + private static final String EMBED_MSG = "Message : " + + "Embedding in google-sitebricks is awesome!"; + + private WebDriver driver; + + public EmbedPage(WebDriver driver) { + this.driver = driver; + } + + public boolean hasCssLink() { + + // UGH, but webdriver is sucking it up with xpath soo.... + return driver.getPageSource().contains(" autobotTextFields = driver.findElements(By.name("autobots")); + + autobotTextFields.get(0).sendKeys(s1); + autobotTextFields.get(1).sendKeys(s2); + autobotTextFields.get(2).sendKeys(s3); + } + + public boolean hasBoundAutobots(String expected) { + return driver.findElement(By.id("boundAutobots")) + .getText() + .contains(expected); + + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/HelloWorldPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/HelloWorldPage.java new file mode 100644 index 00000000..bf04db98 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/HelloWorldPage.java @@ -0,0 +1,38 @@ +package com.google.sitebricks.acceptance.page; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.example.HelloWorld; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.PageFactory; + +public class HelloWorldPage { + + private WebDriver driver; + + public HelloWorldPage(WebDriver driver) { + this.driver = driver; + } + + public boolean hasHelloWorldMessage() { + //TODO ugh! stupid xpath doesn't work =( + return driver.getPageSource().contains(HelloWorld.HELLO_MSG); + } + + public boolean hasCorrectDoctype() { + return driver.getPageSource().startsWith(""); + } + + public boolean hasMangledString() { + return driver.getPageSource().contains(new HelloWorld().mangle(HelloWorld.HELLO_MSG)); + } + + public static HelloWorldPage open(WebDriver driver, String url) { + driver.get(AcceptanceTest.BASE_URL + url); + return PageFactory.initElements(driver, HelloWorldPage.class); + } + + public boolean hasNonSelfClosingScriptTag() { + return driver.getPageSource().contains(""); + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/HiddenFieldMethodPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/HiddenFieldMethodPage.java new file mode 100644 index 00000000..e43ad7cf --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/HiddenFieldMethodPage.java @@ -0,0 +1,42 @@ +package com.google.sitebricks.acceptance.page; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.PageFactory; +import org.testng.annotations.Test; + +/** + * @author Peter Knego + */ +public class HiddenFieldMethodPage { + + private WebDriver driver; + + public HiddenFieldMethodPage(WebDriver driver) { + this.driver = driver; + } + + // "clicks" the submit button + public void submitPut() { + driver.findElement(By.id("put")) + .submit(); + } + + public void enterText(String text) { + driver.findElement(By.name("text")) + .sendKeys(text); + } + + public boolean isPutMessage() { + return driver.findElement(By.id("putMessage")) + .getText() + .endsWith("PUT"); + } + + public static HiddenFieldMethodPage open(WebDriver driver) { + driver.get(AcceptanceTest.BASE_URL + "/hiddenfieldmethod"); + return PageFactory.initElements(driver, HiddenFieldMethodPage.class); + } + +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/I18nPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/I18nPage.java new file mode 100644 index 00000000..197a9b10 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/I18nPage.java @@ -0,0 +1,45 @@ +package com.google.sitebricks.acceptance.page; + +import com.google.inject.Guice; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.client.Web; +import com.google.sitebricks.client.transport.Text; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; + +import java.util.Map; + +public class I18nPage { + private final String content; + public static final String NAME = "Dhanji"; + + public I18nPage(String content) { + try { + this.content = DocumentHelper.parseText(content) + .selectSingleNode("//span['localizedMessage'][1]") + .getText(); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + } + + public boolean hasHelloTo(String name) { + return content.trim().equals("Hello there " + name + "!"); + } + + public boolean hasBonjourTo(String name) { + return content.trim().equals("Bonjour misieu " + name + "!"); + } + + public static I18nPage openWithHeaders(Map headers) { + String content = Guice.createInjector() + .getInstance(Web.class) + .clientOf(AcceptanceTest.BASE_URL + "/i18n?name=" + NAME, headers) + .transports(String.class) + .over(Text.class) + .get() + .toString(); + + return new I18nPage(content); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/PageChainPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/PageChainPage.java new file mode 100644 index 00000000..18010422 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/PageChainPage.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.acceptance.page; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.PageFactory; + +public class PageChainPage { + + private WebDriver driver; + + public PageChainPage(WebDriver driver) { + this.driver = driver; + } + + + public static PageChainPage open(WebDriver driver) { + driver.get(AcceptanceTest.BASE_URL + "/pagechain"); + return PageFactory.initElements(driver, PageChainPage.class); + } + + public void enterText(String someText) { + driver.findElement(By.name("userValue")) + .sendKeys(someText); + } + + public void next() { + driver.findElement(By.id("send")) + .submit(); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/RepeatPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/RepeatPage.java new file mode 100644 index 00000000..fa343352 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/RepeatPage.java @@ -0,0 +1,49 @@ +package com.google.sitebricks.acceptance.page; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import org.openqa.selenium.By; +import org.openqa.selenium.support.How; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +import java.util.ArrayList; +import java.util.List; + +public class RepeatPage { + @FindBy(how = How.XPATH, using = "//div[@class='entry'][1]/ul") + private WebElement namesEntry; + + @FindBy(how = How.XPATH, using = "//div[@class='entry'][2]/ul") + private WebElement moviesEntry; + + private WebDriver driver; + + public RepeatPage(WebDriver driver) { + this.driver = driver; + } + + public List getRepeatedNames() { + List items = new ArrayList(); + for (WebElement li : namesEntry.findElements(By.tagName("li"))) { + items.add(li.getText().trim()); + } + + return items; + } + + public List getRepeatedMovies() { + List items = new ArrayList(); + for (WebElement li : moviesEntry.findElements(By.tagName("li"))) { + items.add(li.getText()); + } + + return items; + } + + public static RepeatPage open(WebDriver driver) { + driver.get(AcceptanceTest.BASE_URL + "/repeat"); + return PageFactory.initElements(driver, RepeatPage.class); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/SelectRoutingPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/SelectRoutingPage.java new file mode 100644 index 00000000..4afc6746 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/SelectRoutingPage.java @@ -0,0 +1,44 @@ +package com.google.sitebricks.acceptance.page; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.example.HelloWorld; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.PageFactory; + +import java.util.List; + +public class SelectRoutingPage { + + private WebDriver driver; + + public SelectRoutingPage(WebDriver driver) { + this.driver = driver; + } + + public boolean hasExpectedDiv(String className) { + try { + WebElement element = driver.findElement(By.className(className)); + } catch (Exception e) { + return false; + } + + return true; + } + + public boolean hasExpectedDivCount(int i) { + List elements = driver.findElements(By.className("result")); + return elements.size() == i; + } + + public static SelectRoutingPage open(WebDriver driver) { + driver.get(AcceptanceTest.BASE_URL + "/select"); + return PageFactory.initElements(driver, SelectRoutingPage.class); + } + + public void submit(String s) { + driver.findElement(By.id(s + "Submit")).submit(); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/StatsPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/StatsPage.java new file mode 100644 index 00000000..2e7bb3d5 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/StatsPage.java @@ -0,0 +1,32 @@ +package com.google.sitebricks.acceptance.page; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.example.Start; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.PageFactory; + +public class StatsPage { + private WebDriver driver; + + public StatsPage(WebDriver driver) { + this.driver = driver; + } + + public boolean hasNonZeroStats() { + String pageSource = driver.getPageSource(); + int pageLoadsStart = pageSource.indexOf(Start.PAGE_LOADS); + // the format is as follows: label:value
+ pageLoadsStart += Start.PAGE_LOADS.length(); + pageLoadsStart += ":".length(); + + int value = Integer.parseInt( + pageSource.substring(pageLoadsStart, pageSource.indexOf("
")).trim()); + + return value > 0; + } + + public static StatsPage open(WebDriver driver) { + driver.get(AcceptanceTest.BASE_URL + "/stats"); + return PageFactory.initElements(driver, StatsPage.class); + } +} \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/util/AcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/util/AcceptanceTest.java new file mode 100644 index 00000000..182b29fd --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/util/AcceptanceTest.java @@ -0,0 +1,16 @@ +package com.google.sitebricks.acceptance.util; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.htmlunit.HtmlUnitDriver; + +/** + * @author Tom Wilson (tom@tomwilson.name) + */ +public class AcceptanceTest { + public static final String SUITE = "acceptance"; + public static final String BASE_URL = "http://localhost:4040/sitebricks"; + + public static WebDriver createWebDriver() { + return new HtmlUnitDriver(); + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/util/Jetty.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/util/Jetty.java new file mode 100644 index 00000000..73a6efc5 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/util/Jetty.java @@ -0,0 +1,45 @@ +package com.google.sitebricks.acceptance.util; + +import org.mortbay.jetty.Server; +import org.mortbay.jetty.webapp.WebAppContext; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class Jetty { + private static final String APP_NAME = "/sitebricks"; + private static final int PORT = 4040; + + private final Server server; + + public Jetty() { + this(new WebAppContext("src/main/resources", APP_NAME), PORT); + } + + public Jetty(String path) { + this(new WebAppContext(path, APP_NAME), PORT); + } + + public Jetty(WebAppContext webAppContext, int port) { + server = new Server(port); + server.addHandler(webAppContext); + } + + public void start() throws Exception { + server.start(); + } + + public void join() throws Exception { + server.join(); + } + + public void stop() throws Exception { + server.stop(); + } + + public static void main(String... args) throws Exception { + Jetty jetty = new Jetty(); + jetty.start(); + jetty.join(); + } +} diff --git a/sitebricks-client/pom.xml b/sitebricks-client/pom.xml new file mode 100644 index 00000000..9db9dda8 --- /dev/null +++ b/sitebricks-client/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + com.google.sitebricks + sitebricks-parent + 0.8.5 + + sitebricks-client + Sitebricks :: Client + + + + org.testng + testng + ${org.testng.version} + jdk15 + test + + + com.google.sitebricks + sitebricks-converter + + + net.jcip + jcip-annotations + + + com.google.inject + guice + + + com.google.inject.extensions + guice-servlet + + + com.ning + async-http-client + + + commons-io + commons-io + + + javax.servlet + servlet-api + + + com.thoughtworks.xstream + xstream + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-mapper-asl + + + diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/AHCWebClient.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/AHCWebClient.java new file mode 100644 index 00000000..446c41cd --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/AHCWebClient.java @@ -0,0 +1,152 @@ +package com.google.sitebricks.client; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import com.ning.http.client.Realm; +import com.ning.http.client.RequestBuilder; +import com.ning.http.client.Response; +import net.jcip.annotations.ThreadSafe; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +/** + * @author Jeanfrancois Arcand (jfarcand@apache.org) + */ +@ThreadSafe + //@Concurrent +class AHCWebClient implements WebClient { + private final Injector injector; + private final String url; + private final Map headers; + private final Class transporting; + private final Key transport; + private final AsyncHttpClient httpClient; + + private final Web.Auth authType; + private final String username; + private final String password; + + public AHCWebClient(Injector injector, Web.Auth authType, String username, + String password, String url, Map headers, + Class transporting, + Key transport) { + + this.injector = injector; + + this.url = url; + this.headers = (null == headers) ? null : ImmutableMap.copyOf(headers); + + + this.authType = authType; + this.username = username; + this.password = password; + this.transporting = transporting; + this.transport = transport; + + // configure auth + AsyncHttpClientConfig.Builder c = new AsyncHttpClientConfig.Builder(); + if (null != authType) { + Realm.RealmBuilder b = new Realm.RealmBuilder(); + // TODO: Add support for Kerberos and SPNEGO + Realm.AuthScheme scheme = authType.equals(Web.Auth.BASIC) ? Realm.AuthScheme.BASIC : Realm.AuthScheme.DIGEST; + b.setPrincipal(username).setPassword(password).setScheme(scheme); + c.setRealm(b.build()); + } + + this.httpClient = new AsyncHttpClient(c.build()); + + } + + private static URI toUri(String url) { + try { + return new URI(url); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + private WebResponse simpleRequest(RequestBuilder requestBuilder) { + + //set request headers as necessary + if (null != headers) + for (Map.Entry header : headers.entrySet()) + requestBuilder.addHeader(header.getKey(), header.getValue()); + + try { + + Response r = httpClient.executeRequest(requestBuilder.build()).get(); + + return new WebResponseImpl(injector, r); + } catch (IOException e) { + throw new TransportException(e); + } catch (InterruptedException e) { + throw new TransportException(e); + } catch (ExecutionException e) { + throw new TransportException(e); + } + } + + + private WebResponse request(RequestBuilder requestBuilder, T t) { + + //set request headers as necessary + if (null != headers) + for (Map.Entry header : headers.entrySet()) + requestBuilder.addHeader(header.getKey(), header.getValue()); + + //fire method + try { + + // Read the entity from the transport plugin. + final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + injector.getInstance(transport) + .out(stream, transporting, t); + + // TODO worry about endian issues? Or will Content-Encoding be sufficient? + // OOM if the stream is too bug + final byte[] outBuffer = stream.toByteArray(); + + //set request body + requestBuilder.setBody(outBuffer); + + Response r = httpClient.executeRequest(requestBuilder.build()).get(); + + return new WebResponseImpl(injector, r); + } catch (IOException e) { + throw new TransportException(e); + } catch (InterruptedException e) { + throw new TransportException(e); + } catch (ExecutionException e) { + throw new TransportException(e); + } + } + + public WebResponse get() { + return simpleRequest((new RequestBuilder("GET")).setUrl(url)); + } + + public WebResponse post(T t) { + return request((new RequestBuilder("POST")).setUrl(url), t); + } + + public WebResponse put(T t) { + return request((new RequestBuilder("PUT")).setUrl(url), t); + } + + public WebResponse delete() { + return simpleRequest((new RequestBuilder("DELETE")).setUrl(url)); + } + + @Override + public void close() { + httpClient.close(); + } +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/CommonsWeb.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/CommonsWeb.java new file mode 100644 index 00000000..3de347e2 --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/CommonsWeb.java @@ -0,0 +1,28 @@ +package com.google.sitebricks.client; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import net.jcip.annotations.Immutable; + +import java.util.Map; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) +*/ +@Immutable +class CommonsWeb implements Web { + private final Provider builder; + + @Inject + public CommonsWeb(Provider builder) { + this.builder = builder; + } + + public FormatBuilder clientOf(String url) { + return builder.get().clientOf(url, null); + } + + public FormatBuilder clientOf(String url, Map headers) { + return builder.get().clientOf(url, headers); + } +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/Transport.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/Transport.java new file mode 100644 index 00000000..7186e48e --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/Transport.java @@ -0,0 +1,48 @@ +package com.google.sitebricks.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public interface Transport { + /** + * Reads from a given inputstream and returns an object representing the unmarshalled + * form of the underlying protocol data. + * + * @param in An inputstream to read data from. This stream will NOT be closed + * by the implementation of this interface. + * @param type The type to read in. The method will return an instance of this + * type representing the data in the {@code in} stream. + * @return An instance of {@code type} representing the data in the provided + * stream. + * + * @throws IOException Thrown if there is an error reading from this stream. + */ + T in(InputStream in, Class type) throws IOException; + + /** + * Converts a given object into transportable data and writes it to the provided + * OutputStream. + * + * @param out An open outputstream to write to. This stream will NOT be closed. + * @param type The type of the data being serialized to the stream. + * @param data An object representing the data to be written out. + + * @throws IOException Thrown if there is an error writing to this stream. + */ + void out(OutputStream out, Class type, T data) throws IOException; + + /** + * Returns the HTTP content type marshalled by this transport. For example, the + * {@link com.google.sitebricks.client.transport.Text} transport transforms plain + * strings to and from the HTTP stream. Its content type is {@code text/plain}. This + * is only a default (or suggested) content type. You may of course, return whatever + * content type is most suitable if using this transport to deliver web responses. + * + * @return A non-empty string representing a HTTP content (mime) type. + */ + String contentType(); +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/TransportException.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/TransportException.java new file mode 100644 index 00000000..72209a29 --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/TransportException.java @@ -0,0 +1,10 @@ +package com.google.sitebricks.client; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public final class TransportException extends RuntimeException { + public TransportException(Throwable cause) { + super(cause); + } +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/Web.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/Web.java new file mode 100644 index 00000000..c68eb744 --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/Web.java @@ -0,0 +1,29 @@ +package com.google.sitebricks.client; + +import com.google.inject.ImplementedBy; + +import java.util.Map; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ImplementedBy(CommonsWeb.class) +public interface Web { + enum Auth { + BASIC, DIGEST + } + + FormatBuilder clientOf(String url); + + FormatBuilder clientOf(String url, Map headers); + + static interface FormatBuilder { + ReadAsBuilder transports(Class clazz); + + FormatBuilder auth(Auth auth, String username, String password); + } + + static interface ReadAsBuilder { + WebClient over(Class clazz); + } +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/WebClient.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/WebClient.java new file mode 100644 index 00000000..12616e08 --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/WebClient.java @@ -0,0 +1,19 @@ +package com.google.sitebricks.client; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public interface WebClient { + WebResponse get(); + + WebResponse post(T t); + + WebResponse put(T t); + + WebResponse delete(); + + /** + * Close the underlying client. + */ + void close(); +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/WebClientBuilder.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/WebClientBuilder.java new file mode 100644 index 00000000..d215e7fc --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/WebClientBuilder.java @@ -0,0 +1,72 @@ +package com.google.sitebricks.client; + +import com.google.common.base.Preconditions; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import net.jcip.annotations.NotThreadSafe; + +import java.util.Map; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@NotThreadSafe +class WebClientBuilder implements Web.FormatBuilder { + + private final Injector injector; + + private String url; + private Map headers; + + private Web.Auth authType; + private String username; + private String password; + + @Inject + public WebClientBuilder(Injector injector) { + this.injector = injector; + } + + public Web.FormatBuilder clientOf(String url) { + this.url = url; + this.headers = null; + + return this; + } + + public Web.FormatBuilder clientOf(String url, Map headers) { + this.url = url; + this.headers = headers; + + return this; + } + + public Web.ReadAsBuilder transports(Class clazz) { + return new InternalReadAsBuilder(clazz); + } + + public Web.FormatBuilder auth(Web.Auth auth, String username, String password) { + Preconditions.checkArgument(null != auth, "Invalid auth type, null."); + Preconditions.checkArgument(null != username, "Username cannot be null."); + Preconditions.checkArgument(null != password, "Password cannot be null."); + + this.authType = auth; + this.username = username; + this.password = password; + return this; + } + + private class InternalReadAsBuilder implements Web.ReadAsBuilder { + private final Class transporting; + + private InternalReadAsBuilder(Class transporting) { + this.transporting = transporting; + } + + public WebClient over(Class transport) { + return new AHCWebClient(injector, authType, username, password, url, + headers, transporting, Key.get(transport)); + } + } +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/WebResponse.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/WebResponse.java new file mode 100644 index 00000000..01e5d991 --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/WebResponse.java @@ -0,0 +1,20 @@ +package com.google.sitebricks.client; + +import java.util.Map; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public interface WebResponse { + Map getHeaders(); + + int status(); + + ResponseTransportBuilder to(Class data); + + String toString(); + + public static interface ResponseTransportBuilder { + T using(Class transport); + } +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/WebResponseImpl.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/WebResponseImpl.java new file mode 100644 index 00000000..a4187a6d --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/WebResponseImpl.java @@ -0,0 +1,89 @@ +package com.google.sitebricks.client; + +import com.google.inject.Injector; +import com.ning.http.client.Response; +import net.jcip.annotations.NotThreadSafe; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + * @author Jeanfrancois Arcand (jfarcand@apache.org) + */ +@NotThreadSafe +class WebResponseImpl implements WebResponse { + private final Injector injector; + private final Response response; + + //memo field + private Map headers; + + public WebResponseImpl(Injector injector, Response response) { + this.injector = injector; + this.response = response; + } + + public Map getHeaders() { + if (null != this.headers) + return this.headers; + + //translate from ahc http client headers + final Map headers = new HashMap(); + for (Map.Entry> header : response.getHeaders().entrySet()) { + for (String value : header.getValue()) { + headers.put(header.getKey(), value); + } + } + + return this.headers = headers; + } + + public ResponseTransportBuilder to(final Class data) { + return new ResponseTransportBuilder() { + + public T using(Class transport) { + + InputStream in = null; + try { + in = response.getResponseBodyAsStream(); + return injector + .getInstance(transport) + .in(in, data); + + } catch (IOException e) { + throw new TransportException(e); + + //ugly stream closing here, to abstract it away from user code + } finally { + try { + if (null != in) + in.close(); + } catch (IOException e) { + //strange, unrecoverable error =( + Logger.getLogger(WebResponseImpl.class.getName()) + .severe("Could not close input stream to in-memory byte array: " + e); + } + } + } + }; + } + + public int status() { + return response.getStatusCode(); + } + + @Override + public String toString() { + try { + return response.getResponseBody(); + } catch (IOException e) { + // TODO + return ""; + } + } +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/ByteArrayTransport.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/ByteArrayTransport.java new file mode 100644 index 00000000..f2d80806 --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/ByteArrayTransport.java @@ -0,0 +1,24 @@ +package com.google.sitebricks.client.transport; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +class ByteArrayTransport extends Xml { + + @SuppressWarnings("unchecked") + public T in(InputStream in, Class type) throws IOException { + assert type == byte[].class; + return (T) IOUtils.toByteArray(in); + } + + public void out(OutputStream out, Class type, T data) throws IOException { + assert data instanceof byte[]; + out.write((byte[]) data); + } +} \ No newline at end of file diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/JacksonJsonTransport.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/JacksonJsonTransport.java new file mode 100644 index 00000000..30785a37 --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/JacksonJsonTransport.java @@ -0,0 +1,169 @@ +package com.google.sitebricks.client.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Set; + +import org.codehaus.jackson.JsonParseException; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.JsonToken; +import org.codehaus.jackson.map.DeserializationContext; +import org.codehaus.jackson.map.JsonDeserializer; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.deser.CustomDeserializerFactory; +import org.codehaus.jackson.map.deser.StdDeserializerProvider; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import com.google.common.primitives.Primitives; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.sitebricks.conversion.Converter; +import com.google.sitebricks.conversion.ConverterRegistry; +import com.google.sitebricks.conversion.StandardTypeConverter; +import com.google.sitebricks.conversion.generics.Generics; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + * @author John Patterson (jdpatterson@gmail.com) + * @author JRodriguez + */ +@Singleton +public class JacksonJsonTransport extends Json { + + private final ObjectMapper objectMapper; + private Collection> exceptions = Sets.newHashSet(); + + @Inject + public JacksonJsonTransport(ConverterRegistry registry) { + this.objectMapper = new ObjectMapper(); + CustomDeserializerFactory deserializerFactory = new CustomDeserializerFactory(); + + // leave these for Jackson to handle + exceptions.add(String.class); + exceptions.add(Object.class); + exceptions.addAll(Primitives.allWrapperTypes()); + + // + Multimap typeToConverterDirection = ArrayListMultimap.create(); + addConverterDirections(registry, true, typeToConverterDirection); + addConverterDirections(registry, false, typeToConverterDirection); + createJacksonDeserializers(deserializerFactory, typeToConverterDirection); + + objectMapper.setDeserializerProvider(new StdDeserializerProvider(deserializerFactory)); + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + // keep track of which direction we want to use + private static class ConverterDirection + { + Converter converter; + boolean forward; + } + + private void addConverterDirections(ConverterRegistry registry, boolean forward, Multimap typeToConverterDirections) { + Multimap> typeToConverters = forward ? registry.getConvertersByTarget() : registry.getConvertersBySource(); + Set types = typeToConverters.keySet(); + for (Type type : types) { + if (exceptions.contains(type)) continue; + Collection> converters = typeToConverters.get(type); + for (Converter converter : converters) { + ConverterDirection converterDirection = new ConverterDirection(); + converterDirection.converter = converter; + converterDirection.forward = forward; + typeToConverterDirections.put(type, converterDirection); + } + } + } + + private void createJacksonDeserializers(CustomDeserializerFactory deserializerFactory, Multimap typeToConverterDirections) + { + Set targetTypes = typeToConverterDirections.keySet(); + for (Type targetType : targetTypes) { + Collection converterDirections = typeToConverterDirections.get(targetType); + Class targetClass = Generics.erase(targetType); + ConvertersDeserializer jds = new ConvertersDeserializer(converterDirections); + typesafeAddMapping(targetClass, jds, deserializerFactory); + } + } + + @SuppressWarnings("unchecked") + private void typesafeAddMapping(Class type, JsonDeserializer deserializer, + CustomDeserializerFactory factory) { + factory.addSpecificMapping((Class) type, deserializer); + } + + public T in(InputStream in, Class type) throws IOException { + return objectMapper.readValue(in, type); + } + + public void out(OutputStream out, Class type, T data) { + try { + objectMapper.writeValue(out, data); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + public class ConvertersDeserializer extends JsonDeserializer { + + private final Collection converterDirections; + + public ConvertersDeserializer(Collection converterDirections) { + this.converterDirections = converterDirections; + } + + public Object deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + + Object source = getSourceObject(jp, ctxt); + + for (ConverterDirection converterDirection : converterDirections) { + + Type sourceType = converterDirection.forward ? + StandardTypeConverter.sourceType(converterDirection.converter) : + StandardTypeConverter.targetType(converterDirection.converter); + + // assume that Jackson only gives us non-generic types + Class converterSourceClass = Generics.erase(sourceType); + + if (converterSourceClass.isAssignableFrom(source.getClass())) { + return converterDirection.forward ? + StandardTypeConverter.typeSafeTo(converterDirection.converter, source) : + StandardTypeConverter.typeSafeFrom(converterDirection.converter, source); + } + } + + throw new IllegalStateException("Cannot convert from " + source); + } + + private Object getSourceObject(JsonParser jp, DeserializationContext ctxt) throws JsonParseException, IOException { + JsonToken t = jp.getCurrentToken(); + if (t == JsonToken.VALUE_NUMBER_INT) { + return jp.getLongValue(); + } + else if (t == JsonToken.VALUE_NUMBER_FLOAT) { + return jp.getDoubleValue(); + } + else if (t == JsonToken.VALUE_TRUE) { + return Boolean.TRUE; + } + else if (t == JsonToken.VALUE_FALSE) { + return Boolean.FALSE; + } + else if (t == JsonToken.VALUE_STRING) { + return jp.getText(); + } + else throw new IllegalStateException(); + } + } +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Json.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Json.java new file mode 100644 index 00000000..e34ab8dc --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Json.java @@ -0,0 +1,18 @@ +package com.google.sitebricks.client.transport; + +import com.google.inject.ImplementedBy; +import com.google.sitebricks.client.Transport; + +/** + * A plain text (UTF-8) implementation of Transport where input types are assumed + * to be Strings. + * + * @author dhanji@google.com (Dhanji R. Prasanna) + */ +@ImplementedBy(JacksonJsonTransport.class) +public abstract class Json implements Transport { + + public String contentType() { + return "text/json"; + } +} \ No newline at end of file diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Raw.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Raw.java new file mode 100644 index 00000000..6b08e84f --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Raw.java @@ -0,0 +1,18 @@ +package com.google.sitebricks.client.transport; + +import com.google.inject.ImplementedBy; +import com.google.sitebricks.client.Transport; + +/** + * A raw implementation of Transport where input types are assumed + * to be byte arrays. + * + * @author dhanji@google.com (Dhanji R. Prasanna) + */ +@ImplementedBy(ByteArrayTransport.class) +public abstract class Raw implements Transport { + + public String contentType() { + return "application/octet-stream"; + } +} \ No newline at end of file diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/SimpleTextTransport.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/SimpleTextTransport.java new file mode 100644 index 00000000..bf7d973f --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/SimpleTextTransport.java @@ -0,0 +1,24 @@ +package com.google.sitebricks.client.transport; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +class SimpleTextTransport extends Text { + public T in(InputStream in, Class type) throws IOException { + return type.cast(IOUtils.toString(in)); + } + + public void out(OutputStream out, Class type, T data) { + try { + IOUtils.write(data.toString(), out); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Text.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Text.java new file mode 100644 index 00000000..b7313c24 --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Text.java @@ -0,0 +1,18 @@ +package com.google.sitebricks.client.transport; + +import com.google.inject.ImplementedBy; +import com.google.sitebricks.client.Transport; + +/** + * A plain text (UTF-8) implementation of Transport where input types are assumed + * to be Strings. + * + * @author dhanji@google.com (Dhanji R. Prasanna) + */ +@ImplementedBy(SimpleTextTransport.class) +public abstract class Text implements Transport { + + public String contentType() { + return "text/plain"; + } +} diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/XStreamXmlTransport.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/XStreamXmlTransport.java new file mode 100644 index 00000000..0c183684 --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/XStreamXmlTransport.java @@ -0,0 +1,28 @@ +package com.google.sitebricks.client.transport; + +import com.google.inject.Inject; +import com.thoughtworks.xstream.XStream; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +class XStreamXmlTransport extends Xml { + private final XStream xStream; + + @Inject + public XStreamXmlTransport(XStream xStream) { + this.xStream = xStream; + } + + public T in(InputStream in, Class type) throws IOException { + return type.cast(xStream.fromXML(in)); + } + + public void out(OutputStream out, Class type, T data) { + xStream.toXML(data, out); + } +} \ No newline at end of file diff --git a/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Xml.java b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Xml.java new file mode 100644 index 00000000..1cef36e0 --- /dev/null +++ b/sitebricks-client/src/main/java/com/google/sitebricks/client/transport/Xml.java @@ -0,0 +1,18 @@ +package com.google.sitebricks.client.transport; + +import com.google.inject.ImplementedBy; +import com.google.sitebricks.client.Transport; + +/** + * A plain text (UTF-8) implementation of Transport where input types are assumed + * to be Strings. + * + * @author dhanji@google.com (Dhanji R. Prasanna) + */ +@ImplementedBy(XStreamXmlTransport.class) +public abstract class Xml implements Transport { + + public String contentType() { + return "text/xml"; + } +} \ No newline at end of file diff --git a/sitebricks-client/src/test/java/com/google/sitebricks/client/WebClientEdslIntegrationTest.java b/sitebricks-client/src/test/java/com/google/sitebricks/client/WebClientEdslIntegrationTest.java new file mode 100644 index 00000000..61f9d2c8 --- /dev/null +++ b/sitebricks-client/src/test/java/com/google/sitebricks/client/WebClientEdslIntegrationTest.java @@ -0,0 +1,43 @@ +package com.google.sitebricks.client; + +import com.google.inject.Guice; +import com.google.sitebricks.client.transport.Text; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class WebClientEdslIntegrationTest { + +// @Test DISABLED + public final void edslForBinding() { + Web resource = Guice.createInjector().getInstance(Web.class); + + WebClient webClient = resource.clientOf("http://google.com") + .transports(String.class) + .over(Text.class); + + final WebResponse response = webClient.get(); + + final String responseAsString = response.toString(); + + assert responseAsString.contains("Google"); + } + +// @Test DISABLED + public final void edslForBasicAuth() { + Web resource = Guice.createInjector().getInstance(Web.class); + + WebClient webClient = resource.clientOf("http://twitter.com") + .auth(Web.Auth.BASIC, "dhanji@gmail.com", "mypass") + .transports(String.class) + .over(Text.class); + + final WebResponse response = webClient.get(); + + final String responseAsString = response.toString(); + + System.out.println(responseAsString); + + webClient.close(); + } +} diff --git a/sitebricks-client/src/test/java/com/google/sitebricks/client/WebClientIntegrationTest.java b/sitebricks-client/src/test/java/com/google/sitebricks/client/WebClientIntegrationTest.java new file mode 100644 index 00000000..1c53cad7 --- /dev/null +++ b/sitebricks-client/src/test/java/com/google/sitebricks/client/WebClientIntegrationTest.java @@ -0,0 +1,24 @@ +package com.google.sitebricks.client; + +import com.google.inject.Guice; +import com.google.sitebricks.client.transport.Text; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + * @Expensive + */ +public class WebClientIntegrationTest { + +// @Test Disabled as you need to be online for this to work + public final void simpleJsonGetFromTwitter() { + Web web = Guice.createInjector().getInstance(Web.class); + + WebClient webClient = web.clientOf("http://twitter.com/statuses/public_timeline.json") + .transports(String.class) + .over(Text.class); + + final WebResponse response = webClient.get(); + + assert response.toString().contains("statuses"); + } +} diff --git a/sitebricks-client/src/test/java/com/google/sitebricks/client/transport/RawTransportTest.java b/sitebricks-client/src/test/java/com/google/sitebricks/client/transport/RawTransportTest.java new file mode 100644 index 00000000..70bc3df0 --- /dev/null +++ b/sitebricks-client/src/test/java/com/google/sitebricks/client/transport/RawTransportTest.java @@ -0,0 +1,39 @@ +package com.google.sitebricks.client.transport; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Unit test for the various transports supported out of the box. + */ +public class RawTransportTest { + private static final String TEXT_DATA = "text"; + + @DataProvider(name = TEXT_DATA) + public Object[][] textData() { + return new Object[][] { + { "Hello there 2793847!@(*&#(!*@&#ASDJFA M??X{." }, + { "\\ \n \n \t \n \0 oaijsdfoijasdoifjao;sidjf19823749872w34*@(#$*&BMBMB" }, + { "19827981273981723981729387192837912873912873" }, + { " " }, + { getClass().toString() }, + { System.getProperties().toString() }, + }; + } + + @Test(dataProvider = TEXT_DATA) + public final void textTransport(String data) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + new ByteArrayTransport().out(out, byte[].class, data.getBytes()); + + // Convert back from byte array to string. + String in = new String(new ByteArrayTransport() + .in(new ByteArrayInputStream(out.toByteArray()), byte[].class)); + + assert data.equals(in) : "Text transport was not balanced"; + } +} diff --git a/sitebricks-client/src/test/java/com/google/sitebricks/client/transport/SimpleTextTransportTest.java b/sitebricks-client/src/test/java/com/google/sitebricks/client/transport/SimpleTextTransportTest.java new file mode 100644 index 00000000..c98da866 --- /dev/null +++ b/sitebricks-client/src/test/java/com/google/sitebricks/client/transport/SimpleTextTransportTest.java @@ -0,0 +1,37 @@ +package com.google.sitebricks.client.transport; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Unit test for the various transports supported out of the box. + */ +public class SimpleTextTransportTest { + private static final String TEXT_DATA = "text"; + + @DataProvider(name = TEXT_DATA) + public Object[][] textData() { + return new Object[][] { + { "Hello there 2793847!@(*&#(!*@&#ASDJFA M??X{." }, + { "\\ \n \n \t \n \0 oaijsdfoijasdoifjao;sidjf19823749872w34*@(#$*&BMBMB" }, + { "19827981273981723981729387192837912873912873" }, + { " " }, + { getClass().toString() }, + { System.getProperties().toString() }, + }; + } + + @Test(dataProvider = TEXT_DATA) + public final void textTransport(String data) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + new SimpleTextTransport().out(out, String.class, data); + + String in = new SimpleTextTransport().in(new ByteArrayInputStream(out.toByteArray()), String.class); + + assert data.equals(in) : "Text transport was not balanced"; + } +} diff --git a/sitebricks-client/src/test/java/com/google/sitebricks/client/transport/XmlTransportTest.java b/sitebricks-client/src/test/java/com/google/sitebricks/client/transport/XmlTransportTest.java new file mode 100644 index 00000000..701daeee --- /dev/null +++ b/sitebricks-client/src/test/java/com/google/sitebricks/client/transport/XmlTransportTest.java @@ -0,0 +1,78 @@ +package com.google.sitebricks.client.transport; + +import com.thoughtworks.xstream.XStream; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Date; + +/** + * Unit test for the xml transport supported out of the box. + */ +public class XmlTransportTest { + private static final String ROBOTS = "robots"; + + @DataProvider(name = ROBOTS) + Object[][] objects() { + return new Object[][] { + { new Robot("megatron", new Date(), 333, 12887L, null) }, + { new Robot("meg aoskdoaks", new Date(192839), 3, 12887L, + new Robot("egatron", new Date(), 2193833, 12312887L, null)) }, + { new Robot("iaisdja aijsd", new Date(1293891283), 333, 12887L, null) }, + }; + } + + @Test(dataProvider = ROBOTS) + public final void xmlTransport(Robot robot) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + new XStreamXmlTransport(new XStream()).out(out, Robot.class, robot); + Robot in = new XStreamXmlTransport(new XStream()).in(new ByteArrayInputStream(out.toByteArray()), Robot.class); + + assert robot.equals(in) : "Xml transport was not balanced"; + } + + public static class Robot { + public Robot(String name, Date time, int age, long looong, Robot pet) { + this.name = name; + this.time = time; + this.age = age; + this.looong = looong; + this.pet = pet; + } + + private String name; + private Date time; + private int age; + private long looong = 123L; + private Robot pet; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Robot that = (Robot) o; + + if (age != that.age) return false; + if (looong != that.looong) return false; + if (name != null ? !name.equals(that.name) : that.name != null) return false; + if (pet != null ? !pet.equals(that.pet) : that.pet != null) return false; + if (time != null ? !time.equals(that.time) : that.time != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (time != null ? time.hashCode() : 0); + result = 31 * result + age; + result = 31 * result + (int) (looong ^ (looong >>> 32)); + result = 31 * result + (pet != null ? pet.hashCode() : 0); + return result; + } + } +} diff --git a/sitebricks-client/src/test/resources/com/google/sitebricks/client/tweet.json b/sitebricks-client/src/test/resources/com/google/sitebricks/client/tweet.json new file mode 100644 index 00000000..f7f2d379 --- /dev/null +++ b/sitebricks-client/src/test/resources/com/google/sitebricks/client/tweet.json @@ -0,0 +1,19 @@ +{ "user": + { "followers_count":303, + "description":"Hello! I'm new here :D", + "url":"", + "profile_image_url":"http:\/\/static.twitter.com\/images\/default_profile_normal.png", + "protected":false,"location":"", + "screen_name":"misterbr", + "name":"Mister B", + "id":21392498}, + + "text":"Watch Football Online - http:\/\/watch-football-live.co.cc\/about", + "truncated":false, + "favorited":false, + "in_reply_to_user_id":null, + "created_at":"Sun Apr 05 11:44:54 +0000 2009", + "source":"web", + "in_reply_to_status_id":null, + "id":1456580694 +} \ No newline at end of file diff --git a/sitebricks-converter/pom.xml b/sitebricks-converter/pom.xml new file mode 100644 index 00000000..e77cbe57 --- /dev/null +++ b/sitebricks-converter/pom.xml @@ -0,0 +1,44 @@ + + 4.0.0 + + com.google.sitebricks + sitebricks-parent + 0.8.5 + + sitebricks-converter + Sitebricks :: Type Conversion + + + + org.testng + testng + ${org.testng.version} + jdk15 + test + + + org.mvel + mvel2 + + + com.google.guava + guava + + + com.google.inject + guice + + + com.google.inject.extensions + guice-multibindings + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-mapper-asl + + + diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/Converter.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/Converter.java new file mode 100644 index 00000000..f246b8c2 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/Converter.java @@ -0,0 +1,18 @@ +package com.google.sitebricks.conversion; + +/** + * Convert an instance from type Source to type Target and back again. + * + * Returning null indicates that the conversion was not successful and another + * converter may be given the chance to handle it. Therefore, null is not a + * valid converted value and null will never be passed as a parameter. + * + * @author John Patterson (jdpatterson@gmail.com) + * + * @param Source Type + * @param Target Type + */ +public interface Converter { + T to(S source); + S from(T target); +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterAdaptor.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterAdaptor.java new file mode 100644 index 00000000..3976e0a8 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterAdaptor.java @@ -0,0 +1,11 @@ +package com.google.sitebricks.conversion; + +/** + * @author John Patterson (jdpatterson@gmail.com) + */ +abstract class ConverterAdaptor implements Converter { + @Override + public S from(T target) { + return null; + } +} \ No newline at end of file diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterRegistry.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterRegistry.java new file mode 100644 index 00000000..c23137db --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterRegistry.java @@ -0,0 +1,15 @@ +package com.google.sitebricks.conversion; + +import java.lang.reflect.Type; +import java.util.Collection; + +import com.google.common.collect.Multimap; +import com.google.inject.ImplementedBy; + +@ImplementedBy(StandardTypeConverter.class) +public interface ConverterRegistry { + void register(Converter converter); + Multimap> getConvertersByTarget(); + Multimap> getConvertersBySource(); + Collection> converter(Type source, Type target); +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterUtils.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterUtils.java new file mode 100644 index 00000000..4e955ee6 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ConverterUtils.java @@ -0,0 +1,34 @@ +package com.google.sitebricks.conversion; + +import com.google.inject.multibindings.Multibinder; + +public class ConverterUtils { + // + // I need to pass in the Multibinder because order in which bindings are made affects the + // outcome of the tests. So I would prefer to just hand back the Multibinder creating it from + // scratch but we can't right now. jvz. (yes, this class won't be around long) + // + public static Multibinder createConverterMultibinder(Multibinder converters) { + // register the default converters after user converters + converters.addBinding().to(ObjectToStringConverter.class); + + // allow single request parameters to be converted to List + converters.addBinding().to(SingletonListConverter.class); + + for( Converter converter : StringToPrimitiveConverters.converters() ) + { + converters.addBinding().toInstance(converter); + } + + for( Converter converter : NumberConverters.converters() ) + { + converters.addBinding().toInstance(converter); + } + + for( Class> converterClass : DateConverters.converters()) + { + converters.addBinding().to(converterClass); + } + return converters; + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/DateConverters.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/DateConverters.java new file mode 100644 index 00000000..44190eb2 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/DateConverters.java @@ -0,0 +1,170 @@ +package com.google.sitebricks.conversion; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +/** + * @author JRodriguez + * @author John Patterson (jdpatterson@gmail.com) + */ +public class DateConverters { + public static List>> converters() { + List>> converters = new ArrayList>>(); + converters.add(LocalizedDateStringConverter.class); + converters.add(DateLongConverter.class); + converters.add(DateCalendarConverter.class); + converters.add(CalendarLongConverter.class); + converters.add(CalendarStringConverter.class); + return converters; + } + + public static class DateLongConverter implements Converter { + @Override + public Date from(Long source) { + return new Date(source); + } + + @Override + public Long to(Date target) { + return target.getTime(); + } + } + + public static class DateCalendarConverter implements Converter { + + @Override + public Calendar to(Date source) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(source); + return calendar; + } + + @Override + public Date from(Calendar target) { + return target.getTime(); + } + } + + public static class DateStringConverter implements Converter { + + protected DateFormat format; + + public DateStringConverter() { + this.format = DateFormat.getInstance(); + } + + public DateStringConverter(DateFormat format) { + this.format = format; + } + + public DateStringConverter(String format) { + this.format = new SimpleDateFormat(format); + } + + @Override + public Date from(String source) { + try { + return getFormat().parse(source); + } + catch (ParseException e) { + throw new IllegalArgumentException("Invalid date format", e); + } + } + + @Override + public String to(Date target) { + return format.format(target); + } + + protected DateFormat getFormat() { + return format; + } + } + + public static class LocalizedDateStringConverter extends DateStringConverter { + + private int dateStyle; + private int timeStyle; + private Provider provider; + + public LocalizedDateStringConverter() { + this(DateFormat.LONG, DateFormat.LONG); + } + public LocalizedDateStringConverter(int dateStyle, int timeStyle) { + this.dateStyle = dateStyle; + this.timeStyle = timeStyle; + } + + @Inject(optional=true) + public void setLocaleProvider(Provider provider) { + this.provider = provider; + } + + @Override + protected DateFormat getFormat() { + if (provider != null) { + return DateFormat.getDateTimeInstance(dateStyle, timeStyle, provider.get()); + } + else { + return super.getFormat(); + } + } + } + + public static class CalendarStringConverter implements Converter { + + private final Provider provider; + + @Inject + public CalendarStringConverter(Provider provider) { + this.provider = provider; + } + + @Override + public String to(Calendar source) { + TypeConverter converter = provider.get(); + Date date = converter.convert(source, Date.class); + return converter.convert(date, String.class); + } + + @Override + public Calendar from(String target) { + TypeConverter converter = provider.get(); + Date date = converter.convert(target, Date.class); + return converter.convert(date, Calendar.class); + } + } + + public static class CalendarLongConverter implements Converter { + + private final Provider provider; + + @Inject + public CalendarLongConverter(Provider provider) { + this.provider = provider; + } + + @Override + public Long to(Calendar source) { + TypeConverter converter = provider.get(); + Date date = converter.convert(source, Date.class); + return converter.convert(date, Long.class); + } + + @Override + public Calendar from(Long target) { + TypeConverter converter = provider.get(); + Date date = converter.convert(target, Date.class); + return converter.convert(date, Calendar.class); + } + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/DummyTypeConverter.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/DummyTypeConverter.java new file mode 100644 index 00000000..9daa288e --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/DummyTypeConverter.java @@ -0,0 +1,19 @@ +package com.google.sitebricks.conversion; + +import java.lang.reflect.Type; + +/** + * Noop implementation that returns the source unaltered. + * + * @author John Patterson (jdpatterson@gmail.com) + * + */ +public class DummyTypeConverter implements TypeConverter +{ + @SuppressWarnings("unchecked") + @Override + public T convert(Object source, Type type) + { + return (T) source; + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/MvelConversionHandlers.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/MvelConversionHandlers.java new file mode 100644 index 00000000..3f56bb35 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/MvelConversionHandlers.java @@ -0,0 +1,69 @@ +package com.google.sitebricks.conversion; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; + +import org.mvel2.ConversionHandler; +import org.mvel2.DataConversion; + +import com.google.common.primitives.Primitives; +import com.google.inject.Inject; +import com.google.sitebricks.conversion.generics.Generics; + +/** + * @author John Patterson (jdpatterson@gmail.com) + */ +public class MvelConversionHandlers { + + private TypeConverter delegate; + private ConverterRegistry registry; + + @Inject + public void prepare(ConverterRegistry registry, TypeConverter delegate) { + this.registry = registry; + this.delegate = delegate; + + Collection> converters = registry.getConvertersBySource().values(); + for (Converter converter : converters) { + ParameterizedType converterType = (ParameterizedType) Generics.getExactSuperType( + converter.getClass(), Converter.class); + + Type[] converterParameters = converterType.getActualTypeArguments(); + registerMvelHandler(converterParameters[0]); + registerMvelHandler(converterParameters[1]); + } + } + + private void registerMvelHandler(Type targetType) { + Class targetClass = Generics.erase(targetType); + SitebricksConversionHandler targetHandler = new SitebricksConversionHandler(targetType); + DataConversion.addConversionHandler(targetClass, targetHandler); + if (Primitives.isWrapperType(targetClass)) { + DataConversion.addConversionHandler(Primitives.unwrap(targetClass), targetHandler); + } + } + + private class SitebricksConversionHandler implements ConversionHandler { + private final Type targetType; + + public SitebricksConversionHandler(Type targetType) { + this.targetType = targetType; + } + + @Override + public Object convertFrom(Object in) { + return delegate.convert(in, targetType); + } + + @SuppressWarnings("unchecked") + @Override + public boolean canConvertFrom(@SuppressWarnings("rawtypes") Class cls) { + if (cls == targetType) + return true; + + // check that there is a converter registered for this source type + return registry.converter(Primitives.wrap(cls), targetType) != null; + } + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/MvelTypeConverter.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/MvelTypeConverter.java new file mode 100644 index 00000000..149c76a1 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/MvelTypeConverter.java @@ -0,0 +1,54 @@ +package com.google.sitebricks.conversion; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; + +import org.mvel2.DataConversion; + +import com.google.inject.Singleton; +import com.google.sitebricks.conversion.TypeConverter; + +/** + * @author John Patterson (jdpatterson@gmail.com) + * + */ +@Singleton +public class MvelTypeConverter implements TypeConverter { + + @Override @SuppressWarnings("unchecked") + public T convert(Object source, Type type) { + return (T) DataConversion.convert(source, erase(type)); + } + + /** + * Returns the erasure of the given type. + * Taken from GenTyRef project + * TODO replace this once Sitebricks has internal generics utils + */ + public static Class erase(Type type) { + if (type instanceof Class) { + return (Class) type; + } + else if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getRawType(); + } + else if (type instanceof TypeVariable) { + TypeVariable tv = (TypeVariable) type; + if (tv.getBounds().length == 0) + return Object.class; + else + return erase(tv.getBounds()[0]); + } + else if (type instanceof GenericArrayType) { + GenericArrayType aType = (GenericArrayType) type; + Class componentType = erase(aType.getGenericComponentType()); + return Array.newInstance(componentType, 0).getClass(); + } + else { + throw new RuntimeException("not supported: " + type.getClass()); + } + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/NumberConverters.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/NumberConverters.java new file mode 100644 index 00000000..b598eca6 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/NumberConverters.java @@ -0,0 +1,53 @@ +package com.google.sitebricks.conversion; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +/** + * @author John Patterson (jdpatterson@gmail.com) + */ +public class NumberConverters { + public static List> converters() { + List> converters = new ArrayList>(); + + converters.add(new ConverterAdaptor() { + public Integer to(Number source) { + return Integer.valueOf(source.intValue()); + } + }); + converters.add(new ConverterAdaptor() { + public Long to(Number source) { + return Long.valueOf(source.longValue()); + } + }); + converters.add(new ConverterAdaptor() { + public Float to(Number source) { + return Float.valueOf(source.floatValue()); + } + }); + converters.add(new ConverterAdaptor() { + public Double to(Number source) { + return Double.valueOf(source.doubleValue()); + } + }); + converters.add(new ConverterAdaptor() { + public Short to(Number source) { + return Short.valueOf(source.shortValue()); + } + }); + converters.add(new ConverterAdaptor() { + public BigInteger to(Number source) { + return BigInteger.valueOf(source.longValue()); + } + }); + converters.add(new ConverterAdaptor() { + public BigDecimal to(Number source) { + return BigDecimal.valueOf(source.doubleValue()); + } + }); + + return converters; + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ObjectToStringConverter.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ObjectToStringConverter.java new file mode 100644 index 00000000..f44aa4f7 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/ObjectToStringConverter.java @@ -0,0 +1,12 @@ +package com.google.sitebricks.conversion; + +import com.google.inject.Singleton; + +@Singleton +public class ObjectToStringConverter extends ConverterAdaptor { + + @Override + public String to(Object source) { + return source.toString(); + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/SingletonListConverter.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/SingletonListConverter.java new file mode 100644 index 00000000..58c4a566 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/SingletonListConverter.java @@ -0,0 +1,17 @@ +package com.google.sitebricks.conversion; + +import java.util.Arrays; +import java.util.List; + +public class SingletonListConverter implements Converter> { + + @Override + public List to(Object source) { + return Arrays.asList(source); + } + + @Override + public Object from(List target) { + return target.get(0); + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/StandardTypeConverter.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/StandardTypeConverter.java new file mode 100644 index 00000000..93818d1c --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/StandardTypeConverter.java @@ -0,0 +1,219 @@ +package com.google.sitebricks.conversion; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.primitives.Primitives; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.sitebricks.conversion.generics.Generics; + +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; + +import static com.google.sitebricks.conversion.generics.Generics.erase; +import static com.google.sitebricks.conversion.generics.Generics.getExactSuperType; +import static com.google.sitebricks.conversion.generics.Generics.getTypeParameter; + +/** + * @author John Patterson (jdpatterson@gmail.com) + */ +@Singleton +public class StandardTypeConverter implements TypeConverter, ConverterRegistry { + Multimap> convertersBySource = ArrayListMultimap.create(); + Multimap> convertersByTarget = ArrayListMultimap.create(); + + Multimap> convertersBySourceAndTarget = ArrayListMultimap.create(); + private static final TypeVariable> sourceTypeParameter = Converter.class.getTypeParameters()[0]; + private static final TypeVariable> targetTypeParameter = Converter.class.getTypeParameters()[1]; + + @Inject + public StandardTypeConverter(@SuppressWarnings("rawtypes") Set converters) { + for (Converter converter : converters) { + register(converter); + } + } + + @Override + public void register(Converter converter) { + // get the source and target types + Type sourceType = sourceType(converter); + Type targetType = targetType(converter); + convertersBySource.put(sourceType, converter); + convertersByTarget.put(targetType, converter); + convertersBySourceAndTarget.put(new SourceAndTarget(sourceType, targetType), converter); + } + + public static Type targetType(Converter converter) { + return getTypeParameter(converter.getClass(), targetTypeParameter); + } + + public static Type sourceType(Converter converter) { + return getTypeParameter(converter.getClass(), sourceTypeParameter); + } + + @Override + @SuppressWarnings("unchecked") + public T convert(final Object source, Type type) { + + // special case for handling a null source values + if (source == null) { + return (T) nullValue(type); + } + + // check we already have the exact type + if (source.getClass() == type) { + return (T) source; + } + + // check if we already have a sub type + if (Generics.isSuperType(type, source.getClass())) + { + return (T) source; + } + + // special case for handling empty string + if ("".equals(source) && type != String.class && isEmptyStringNull()) { + return null; + } + + Type sourceType = source.getClass(); + Class sourceClass = Generics.erase(sourceType); + + // conversion of all array types to collections + if (sourceClass.isArray() && Generics.isSuperType(Collection.class, type)) { + return (T) Arrays.asList(source); + } + + // conversion of all collections to arrays + Class targetClass = Generics.erase(type); + if (Collection.class.isAssignableFrom(sourceClass) && targetClass.isArray()) { + // TODO: convert collections to arrays + throw new UnsupportedOperationException("Not implemented yet"); + } + + // use primitive wrapper types + if (type instanceof Class && ((Class) type).isPrimitive()) { + type = Primitives.wrap((Class) type); + } + + + // look for converters for exact types or super types + Object result = null; + do { + + // first try to find a converter in the forward direction + SourceAndTarget key = new SourceAndTarget(sourceType, type); + Collection> forwards = convertersBySourceAndTarget.get(key); + + // stop at the first converter that returns non-null + for (Converter forward : forwards) + if ((result = typeSafeTo(forward, source)) != null) break; + + if (result == null) { + // try the reverse direction (target to source) + Collection> reverses = convertersBySourceAndTarget.get(key.reverse()); + + // stop at the first converter that returns non-null + for (Converter reverse : reverses) + if ((result = typeSafeFrom(reverse, source)) != null) break; + } + + // we have no more super classes to try + if (sourceType == Object.class) break; + + // try every super type of the source + Class superClass = erase(sourceType).getSuperclass(); + sourceType = getExactSuperType(sourceType, superClass); + } while (result == null); + + if (result == null) + throw new IllegalStateException("Cannot convert " + source.getClass() + " to " + type); + + return (T) result; + } + + @Override + public Collection> converter(Type source, Type target) { + SourceAndTarget key = new SourceAndTarget(source, target); + return convertersBySourceAndTarget.get(key); + } + + protected boolean isEmptyStringNull() { + return true; + } + + protected Object nullValue(Type type) { + if (type == String.class) { + return ""; + } + else return null; + } + + @SuppressWarnings("unchecked") + public static T typeSafeTo(Converter converter, S source) { + return ((Converter) converter).to(source); + } + + @SuppressWarnings("unchecked") + public static S typeSafeFrom(Converter converter, T source) { + return ((Converter) converter).from(source); + } + + @Override + public Multimap> getConvertersBySource() { + return convertersBySource; + } + + @Override + public Multimap> getConvertersByTarget() { + return convertersByTarget; + } + + private static final class SourceAndTarget { + private Type source; + private Type target; + + public SourceAndTarget(Type source, Type target) { + this.source = source; + this.target = target; + } + + public SourceAndTarget reverse() { + return new SourceAndTarget(target, source); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((source == null) ? 0 : source.hashCode()); + result = prime * result + ((target == null) ? 0 : target.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SourceAndTarget other = (SourceAndTarget) obj; + if (source == null) { + if (other.source != null) + return false; + } else if (!source.equals(other.source)) + return false; + if (target == null) { + if (other.target != null) + return false; + } else if (!target.equals(other.target)) + return false; + return true; + } + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/StringToPrimitiveConverters.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/StringToPrimitiveConverters.java new file mode 100644 index 00000000..a14992ea --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/StringToPrimitiveConverters.java @@ -0,0 +1,50 @@ +package com.google.sitebricks.conversion; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author John Patterson (jdpatterson@gmail.com) + */ +public class StringToPrimitiveConverters { + public static List> converters() { + List> converters = new ArrayList>(); + converters.add(new ConverterAdaptor() { + public Integer to(String source) { + return Integer.valueOf(source); + } + }); + converters.add(new ConverterAdaptor() { + public Long to(String source) { + return Long.valueOf(source); + } + }); + converters.add(new ConverterAdaptor() { + public Float to(String source) { + return Float.valueOf(source); + } + }); + converters.add(new ConverterAdaptor() { + public Double to(String source) { + return Double.valueOf(source); + } + }); + converters.add(new ConverterAdaptor() { + public Byte to(String source) { + return Byte.valueOf(source); + } + }); + converters.add(new ConverterAdaptor() { + public Boolean to(String source) { + return Boolean.valueOf(source); + } + }); + converters.add(new ConverterAdaptor() { + public Character to(String source) { + return source.charAt(0); + } + }); + + return converters; + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/TypeConverter.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/TypeConverter.java new file mode 100644 index 00000000..62b4e7a9 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/TypeConverter.java @@ -0,0 +1,23 @@ +package com.google.sitebricks.conversion; + +import com.google.inject.ImplementedBy; + +import java.lang.reflect.Type; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + * @author John Patterson (jdpatterson@gmail.com) + * @author JRodriguez + */ +@ImplementedBy(StandardTypeConverter.class) +public interface TypeConverter { + + /** + * Convert an instance to the given type. + * + * @param source Original instance + * @param type The type to convert to. + * @return A converted instance of type {@code Type}} + */ + T convert(Object source, Type type); +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/CaptureType.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/CaptureType.java new file mode 100644 index 00000000..c2f3e019 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/CaptureType.java @@ -0,0 +1,35 @@ +/* + * Copied from Gentyref project http://code.google.com/p/gentyref/ + * Reformatted and moved to fit package structure + */ +package com.google.sitebricks.conversion.generics; + +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; + +/** + * CaptureType represents a wildcard that has gone through capture conversion. + * It is a custom subinterface of Type, not part of the java builtin Type + * hierarchy. + * + * @author Wouter Coekaerts + */ +public interface CaptureType extends Type +{ + /** + * Returns an array of Type objects representing the upper bound(s) + * of this capture. This includes both the upper bound of a + * ? extends wildcard, and the bounds declared with the type + * variable. References to other (or the same) type variables in bounds + * coming from the type variable are replaced by their matching capture. + */ + Type[] getUpperBounds(); + + /** + * Returns an array of Type objects representing the lower bound(s) + * of this type variable. This is the bound of a ? super wildcard. + * This normally contains only one or no types; it is an array for + * consistency with {@link WildcardType#getLowerBounds()}. + */ + Type[] getLowerBounds(); +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/CaptureTypeImpl.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/CaptureTypeImpl.java new file mode 100644 index 00000000..bcc9aac0 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/CaptureTypeImpl.java @@ -0,0 +1,71 @@ +/* + * Copied from Gentyref project http://code.google.com/p/gentyref/ + * Reformatted and moved to fit package structure + */ +package com.google.sitebricks.conversion.generics; + +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Arrays; + +class CaptureTypeImpl implements CaptureType +{ + private final WildcardType wildcard; + private final TypeVariable variable; + private final Type[] lowerBounds; + private Type[] upperBounds; + + /** + * Creates an uninitialized CaptureTypeImpl. Before using this type, + * {@link #init(VarMap)} must be called. + * + * @param wildcard + * The wildcard this is a capture of + * @param variable + * The type variable where the wildcard is a parameter for. + */ + public CaptureTypeImpl(WildcardType wildcard, TypeVariable variable) + { + this.wildcard = wildcard; + this.variable = variable; + this.lowerBounds = wildcard.getLowerBounds(); + } + + /** + * Initialize this CaptureTypeImpl. This is needed for type variable bounds + * referring to each other: we need the capture of the argument. + */ + void init(VarMap varMap) + { + ArrayList upperBoundsList = new ArrayList(); + upperBoundsList.addAll(Arrays.asList(varMap.map(variable.getBounds()))); + upperBoundsList.addAll(Arrays.asList(wildcard.getUpperBounds())); + upperBounds = new Type[upperBoundsList.size()]; + upperBoundsList.toArray(upperBounds); + } + + /* + * @see com.googlecode.gentyref.CaptureType#getLowerBounds() + */ + public Type[] getLowerBounds() + { + return lowerBounds.clone(); + } + + /* + * @see com.googlecode.gentyref.CaptureType#getUpperBounds() + */ + public Type[] getUpperBounds() + { + assert upperBounds != null; + return upperBounds.clone(); + } + + @Override + public String toString() + { + return "capture of " + wildcard; + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/GenericArrayTypeImpl.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/GenericArrayTypeImpl.java new file mode 100644 index 00000000..2d2024fc --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/GenericArrayTypeImpl.java @@ -0,0 +1,64 @@ +/* + * Copied from Gentyref project http://code.google.com/p/gentyref/ + * Code was reformatted and moved to fit package structure + */ +package com.google.sitebricks.conversion.generics; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Type; + +class GenericArrayTypeImpl implements GenericArrayType +{ + private Type componentType; + + static Class createArrayType(Class componentType) + { + // there's no (clean) other way to create a array class, then create an + // instance of it + return Array.newInstance(componentType, 0).getClass(); + } + + static Type createArrayType(Type componentType) + { + if (componentType instanceof Class) + { + return createArrayType((Class) componentType); + } + else + { + return new GenericArrayTypeImpl(componentType); + } + } + + private GenericArrayTypeImpl(Type componentType) + { + super(); + this.componentType = componentType; + } + + public Type getGenericComponentType() + { + return componentType; + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof GenericArrayType)) + return false; + return componentType.equals(((GenericArrayType) obj).getGenericComponentType()); + } + + @Override + public int hashCode() + { + return componentType.hashCode() * 7; + } + + @Override + public String toString() + { + return componentType + "[]"; + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/Generics.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/Generics.java new file mode 100644 index 00000000..06c7233a --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/Generics.java @@ -0,0 +1,547 @@ +/* + * Copied from Gentyref project http://code.google.com/p/gentyref/ + * Code was reformatted and moved to fit package structure + */ +package com.google.sitebricks.conversion.generics; + + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Generics { + private static final Type UNBOUND_WILDCARD = new WildcardTypeImpl(new Type[] { Object.class }, + new Type[] {}); + + /** + * Returns the erasure of the given type. + */ + public static Class erase(Type type) + { + if (type instanceof Class) + { + return (Class) type; + } + else if (type instanceof ParameterizedType) + { + return (Class) ((ParameterizedType) type).getRawType(); + } + else if (type instanceof TypeVariable) + { + TypeVariable tv = (TypeVariable) type; + if (tv.getBounds().length == 0) + return Object.class; + else + return erase(tv.getBounds()[0]); + } + else if (type instanceof GenericArrayType) + { + GenericArrayType aType = (GenericArrayType) type; + return GenericArrayTypeImpl.createArrayType(erase(aType.getGenericComponentType())); + } + else + { + // TODO at least support CaptureType here + throw new RuntimeException("not supported: " + type.getClass()); + } + } + + /** + * Maps type parameters in a type to their values. + * + * @param toMapType + * Type possibly containing type arguments + * @param typeAndParams + * must be either ParameterizedType, or (in case there are no + * type arguments, or it's a raw type) Class + * @return toMapType, but with type parameters from typeAndParams replaced. + */ + private static Type mapTypeParameters(Type toMapType, Type typeAndParams) + { + if (isMissingTypeParameters(typeAndParams)) + { + return erase(toMapType); + } + else + { + VarMap varMap = new VarMap(); + Type handlingTypeAndParams = typeAndParams; + while (handlingTypeAndParams instanceof ParameterizedType) + { + ParameterizedType pType = (ParameterizedType) handlingTypeAndParams; + Class clazz = (Class) pType.getRawType(); // getRawType + // should always + // be Class + varMap.addAll(clazz.getTypeParameters(), pType.getActualTypeArguments()); + handlingTypeAndParams = pType.getOwnerType(); + } + return varMap.map(toMapType); + } + } + + /** + * Checks if the given type is a class that is supposed to have type + * parameters, but doesn't. In other words, if it's a really raw type. + */ + private static boolean isMissingTypeParameters(Type type) + { + if (type instanceof Class) + { + for (Class clazz = (Class) type; clazz != null; clazz = clazz.getEnclosingClass()) + { + if (clazz.getTypeParameters().length != 0) + return true; + } + return false; + } + else if (type instanceof ParameterizedType) + { + return false; + } + else + { + throw new AssertionError("Unexpected type " + type.getClass()); + } + } + + /** + * Returns a type representing the class, with all type parameters the + * unbound wildcard ("?"). For example, + * addWildcardParameters(Map.class) returns a type representing + * Map<?,?>. + * + * @return
    + *
  • If clazz is a class or interface without type parameters, + * clazz itself is returned.
  • + *
  • If clazz is a class or interface with type parameters, an + * instance of ParameterizedType is returned.
  • + *
  • if clazz is an array type, an array type is returned with + * unbound wildcard parameters added in the the component type. + *
+ */ + public static Type addWildcardParameters(Class clazz) + { + if (clazz.isArray()) + { + return GenericArrayTypeImpl.createArrayType(addWildcardParameters(clazz + .getComponentType())); + } + else if (isMissingTypeParameters(clazz)) + { + TypeVariable[] vars = clazz.getTypeParameters(); + Type[] arguments = new Type[vars.length]; + Arrays.fill(arguments, UNBOUND_WILDCARD); + Type owner = clazz.getDeclaringClass() == null ? null : addWildcardParameters(clazz + .getDeclaringClass()); + return new ParameterizedTypeImpl(clazz, arguments, owner); + } + else + { + return clazz; + } + } + + /** + * With type a supertype of searchClass, returns the exact supertype of the + * given class, including type parameters. For example, with + * class StringList implements List<String>, + * getExactSuperType(StringList.class, Collection.class) returns a + * {@link ParameterizedType} representing Collection<String>. + *
    + *
  • Returns null if searchClass is not a superclass of type.
  • + *
  • Returns an instance of {@link Class} if type if it is a raw + * type, or has no type parameters
  • + *
  • Returns an instance of {@link ParameterizedType} if the type does + * have parameters
  • + *
  • Returns an instance of {@link GenericArrayType} if + * searchClass is an array type, and the actual type has type + * parameters
  • + *
+ */ + public static Type getExactSuperType(Type type, Class searchClass) + { + if (type instanceof ParameterizedType || type instanceof Class + || type instanceof GenericArrayType) + { + Class clazz = erase(type); + + if (searchClass == clazz) + { + return type; + } + + if (!searchClass.isAssignableFrom(clazz)) + return null; + } + + for (Type superType : getExactDirectSuperTypes(type)) + { + Type result = getExactSuperType(superType, searchClass); + if (result != null) + return result; + } + + return null; + } + + /** + * Gets the type parameter for a given type that is the value for a given + * type variable. For example, with + * class StringList implements List<String>, + * getTypeParameter(StringList.class, Collection.class.getTypeParameters()[0]) + * returns String. + * + * @param type + * The type to inspect. + * @param variable + * The type variable to find the value for. + * @return The type parameter for the given variable. Or null if type is not + * a subtype of the type that declares the variable, or if the + * variable isn't known (because of raw types). + */ + public static Type getTypeParameter(Type type, TypeVariable> variable) + { + Class clazz = variable.getGenericDeclaration(); + Type superType = getExactSuperType(type, clazz); + if (superType instanceof ParameterizedType) + { + int index = Arrays.asList(clazz.getTypeParameters()).indexOf(variable); + return ((ParameterizedType) superType).getActualTypeArguments()[index]; + } + else + { + return null; + } + } + + /** + * Checks if the capture of subType is a subtype of superType + */ + public static boolean isSuperType(Type superType, Type subType) + { + if (superType instanceof ParameterizedType || superType instanceof Class + || superType instanceof GenericArrayType) + { + Class superClass = erase(superType); + Type mappedSubType = getExactSuperType(capture(subType), superClass); + if (mappedSubType == null) + { + return false; + } + else if (superType instanceof Class) + { + return true; + } + else if (mappedSubType instanceof Class) + { + // TODO treat supertype by being raw type differently + // ("supertype, but with warnings") + return true; // class has no parameters, or it's a raw type + } + else if (mappedSubType instanceof GenericArrayType) + { + Type superComponentType = getArrayComponentType(superType); + assert superComponentType != null; + Type mappedSubComponentType = getArrayComponentType(mappedSubType); + assert mappedSubComponentType != null; + return isSuperType(superComponentType, mappedSubComponentType); + } + else + { + assert mappedSubType instanceof ParameterizedType; + ParameterizedType pMappedSubType = (ParameterizedType) mappedSubType; + assert pMappedSubType.getRawType() == superClass; + ParameterizedType pSuperType = (ParameterizedType) superType; + + Type[] superTypeArgs = pSuperType.getActualTypeArguments(); + Type[] subTypeArgs = pMappedSubType.getActualTypeArguments(); + assert superTypeArgs.length == subTypeArgs.length; + for (int i = 0; i < superTypeArgs.length; i++) + { + if (!contains(superTypeArgs[i], subTypeArgs[i])) + { + return false; + } + } + // params of the class itself match, so if the owner types are + // supertypes too, it's a supertype. + return pSuperType.getOwnerType() == null + || isSuperType(pSuperType.getOwnerType(), pMappedSubType.getOwnerType()); + } + } + else if (superType instanceof CaptureType) + { + if (superType.equals(subType)) + return true; + for (Type lowerBound : ((CaptureType) superType).getLowerBounds()) + { + if (isSuperType(lowerBound, subType)) + { + return true; + } + } + return false; + } + else if (superType instanceof GenericArrayType) + { + return isArraySupertype(superType, subType); + } + else + { + throw new RuntimeException("not implemented: " + superType.getClass()); + } + } + + private static boolean isArraySupertype(Type arraySuperType, Type subType) + { + Type superTypeComponent = getArrayComponentType(arraySuperType); + assert superTypeComponent != null; + Type subTypeComponent = getArrayComponentType(subType); + if (subTypeComponent == null) + { // subType is not an array type + return false; + } + else + { + return isSuperType(superTypeComponent, subTypeComponent); + } + } + + /** + * If type is an array type, returns the type of the component of the array. + * Otherwise, returns null. + */ + public static Type getArrayComponentType(Type type) + { + if (type instanceof Class) + { + Class clazz = (Class) type; + return clazz.getComponentType(); + } + else if (type instanceof GenericArrayType) + { + GenericArrayType aType = (GenericArrayType) type; + return aType.getGenericComponentType(); + } + else + { + return null; + } + } + + private static boolean contains(Type containingType, Type containedType) + { + if (containingType instanceof WildcardType) + { + WildcardType wContainingType = (WildcardType) containingType; + for (Type upperBound : wContainingType.getUpperBounds()) + { + if (!isSuperType(upperBound, containedType)) + { + return false; + } + } + for (Type lowerBound : wContainingType.getLowerBounds()) + { + if (!isSuperType(containedType, lowerBound)) + { + return false; + } + } + return true; + } + else + { + return containingType.equals(containedType); + } + } + + /** + * Returns the direct supertypes of the given type. Resolves type + * parameters. + */ + public static Type[] getExactDirectSuperTypes(Type type) + { + if (type instanceof ParameterizedType || type instanceof Class) + { + Class clazz; + if (type instanceof ParameterizedType) + { + clazz = (Class) ((ParameterizedType) type).getRawType(); + } + else + { + // TODO primitive types? + clazz = (Class) type; + if (clazz.isArray()) + return getArrayExactDirectSuperTypes(clazz); + } + + Type[] superInterfaces = clazz.getGenericInterfaces(); + Type superClass = clazz.getGenericSuperclass(); + Type[] result; + int resultIndex; + if (superClass == null) + { + result = new Type[superInterfaces.length]; + resultIndex = 0; + } + else + { + result = new Type[superInterfaces.length + 1]; + resultIndex = 1; + result[0] = mapTypeParameters(superClass, type); + } + for (Type superInterface : superInterfaces) + { + result[resultIndex++] = mapTypeParameters(superInterface, type); + } + + return result; + } + else if (type instanceof TypeVariable) + { + TypeVariable tv = (TypeVariable) type; + return tv.getBounds(); + } + else if (type instanceof WildcardType) + { + // This should be a rare case: normally this wildcard is already + // captured. + // But it does happen if the upper bound of a type variable contains + // a wildcard + // TODO shouldn't upper bound of type variable have been captured + // too? (making this case impossible?) + return ((WildcardType) type).getUpperBounds(); + } + else if (type instanceof CaptureType) + { + return ((CaptureType) type).getUpperBounds(); + } + else if (type instanceof GenericArrayType) + { + return getArrayExactDirectSuperTypes(type); + } + else + { + throw new RuntimeException("not implemented type: " + type); + } + } + + private static Type[] getArrayExactDirectSuperTypes(Type arrayType) + { + // see + // http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.10.3 + Type typeComponent = getArrayComponentType(arrayType); + + Type[] result; + int resultIndex; + if (typeComponent instanceof Class && ((Class) typeComponent).isPrimitive()) + { + resultIndex = 0; + result = new Type[3]; + } + else + { + Type[] componentSupertypes = getExactDirectSuperTypes(typeComponent); + result = new Type[componentSupertypes.length + 3]; + for (resultIndex = 0; resultIndex < componentSupertypes.length; resultIndex++) + { + result[resultIndex] = GenericArrayTypeImpl + .createArrayType(componentSupertypes[resultIndex]); + } + } + result[resultIndex++] = Object.class; + result[resultIndex++] = Cloneable.class; + result[resultIndex++] = Serializable.class; + return result; + } + + /** + * Returns the exact return type of the given method in the given type. This + * may be different from m.getGenericReturnType() when the method + * was declared in a superclass, of type is a raw type. + */ + public static Type getExactReturnType(Method m, Type type) + { + Type returnType = m.getGenericReturnType(); + Type exactDeclaringType = getExactSuperType(capture(type), m.getDeclaringClass()); + return mapTypeParameters(returnType, exactDeclaringType); + } + + /** + * Returns the exact type of the given field in the given type. This may be + * different from f.getGenericType() when the field was declared in + * a superclass, of type is a raw type. + */ + public static Type getExactFieldType(Field f, Type type) + { + Type returnType = f.getGenericType(); + Type exactDeclaringType = getExactSuperType(capture(type), f.getDeclaringClass()); + return mapTypeParameters(returnType, exactDeclaringType); + } + + /** + * Applies capture conversion to the given type. + */ + public static Type capture(Type type) + { + VarMap varMap = new VarMap(); + List toInit = new ArrayList(); + if (type instanceof ParameterizedType) + { + ParameterizedType pType = (ParameterizedType) type; + Class clazz = (Class) pType.getRawType(); + Type[] arguments = pType.getActualTypeArguments(); + TypeVariable[] vars = clazz.getTypeParameters(); + Type[] capturedArguments = new Type[arguments.length]; + assert arguments.length == vars.length; + for (int i = 0; i < arguments.length; i++) + { + Type argument = arguments[i]; + if (argument instanceof WildcardType) + { + CaptureTypeImpl captured = new CaptureTypeImpl((WildcardType) argument, vars[i]); + argument = captured; + toInit.add(captured); + } + capturedArguments[i] = argument; + varMap.add(vars[i], argument); + } + for (CaptureTypeImpl captured : toInit) + { + captured.init(varMap); + } + Type ownerType = (pType.getOwnerType() == null) ? null : capture(pType.getOwnerType()); + return new ParameterizedTypeImpl(clazz, capturedArguments, ownerType); + } + else + { + return type; + } + } + + /** + * Returns the display name of a Type. + */ + public static String getTypeName(Type type) + { + if (type instanceof Class) + { + Class clazz = (Class) type; + return clazz.isArray() ? (getTypeName(clazz.getComponentType()) + "[]") : clazz.getName(); + } + else + { + return type.toString(); + } + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/ParameterizedTypeImpl.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/ParameterizedTypeImpl.java new file mode 100644 index 00000000..5a67f0fd --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/ParameterizedTypeImpl.java @@ -0,0 +1,95 @@ +/* + * Copied from Gentyref project http://code.google.com/p/gentyref/ + * Reformatted and moved to fit package structure + */ +package com.google.sitebricks.conversion.generics; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; + +public class ParameterizedTypeImpl implements ParameterizedType +{ + private final Class rawType; + private final Type[] actualTypeArguments; + private final Type ownerType; + + public ParameterizedTypeImpl(Class rawType, Type[] actualTypeArguments, Type ownerType) + { + this.rawType = rawType; + this.actualTypeArguments = actualTypeArguments; + this.ownerType = ownerType; + } + + public Type getRawType() + { + return rawType; + } + + public Type[] getActualTypeArguments() + { + return actualTypeArguments; + } + + public Type getOwnerType() + { + return ownerType; + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof ParameterizedType)) + return false; + + ParameterizedType other = (ParameterizedType) obj; + return rawType.equals(other.getRawType()) + && Arrays.equals(actualTypeArguments, other.getActualTypeArguments()) + && (ownerType == null ? other.getOwnerType() == null : ownerType.equals(other + .getOwnerType())); + } + + @Override + public int hashCode() + { + int result = rawType.hashCode() ^ Arrays.hashCode(actualTypeArguments); + if (ownerType != null) + result ^= ownerType.hashCode(); + return result; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + String clazz = rawType.getName(); + + if (ownerType != null) + { + sb.append(Generics.getTypeName(ownerType)).append('.'); + + String prefix = (ownerType instanceof ParameterizedType) ? ((Class) ((ParameterizedType) ownerType) + .getRawType()).getName() + '$' + : ((Class) ownerType).getName() + '$'; + if (clazz.startsWith(prefix)) + clazz = clazz.substring(prefix.length()); + } + sb.append(clazz); + + if (actualTypeArguments.length != 0) + { + sb.append('<'); + for (int i = 0; i < actualTypeArguments.length; i++) + { + Type arg = actualTypeArguments[i]; + if (i != 0) + sb.append(", "); + sb.append(Generics.getTypeName(arg)); + } + sb.append('>'); + } + + return sb.toString(); + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/TypeToken.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/TypeToken.java new file mode 100644 index 00000000..e0c7f063 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/TypeToken.java @@ -0,0 +1,97 @@ +/* + * Copied from Gentyref project http://code.google.com/p/gentyref/ + * Reformatted and moved to fit package structure + */ + +package com.google.sitebricks.conversion.generics; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * Wrapper around {@link Type}. + * + * You can use this to create instances of Type for a type known at compile + * time. + * + * For example, to get the Type that represents List<String>: + * Type listOfString = new TypeToken<List<String>>(){}.getType(); + * + * @author Wouter Coekaerts + * + * @param + * The type represented by this TypeToken. + */ +public abstract class TypeToken +{ + private final Type type; + + /** + * Constructs a type token. + */ + protected TypeToken() + { + this.type = extractType(); + } + + private TypeToken(Type type) + { + this.type = type; + } + + public Type getType() + { + return type; + } + + private Type extractType() + { + Type t = getClass().getGenericSuperclass(); + if (!(t instanceof ParameterizedType)) + { + throw new RuntimeException("Invalid TypeToken; must specify type parameters"); + } + ParameterizedType pt = (ParameterizedType) t; + if (pt.getRawType() != TypeToken.class) + { + throw new RuntimeException("Invalid TypeToken; must directly extend TypeToken"); + } + return pt.getActualTypeArguments()[0]; + } + + /** + * Gets type token for the given {@code Class} instance. + */ + public static TypeToken get(Class type) + { + return new SimpleTypeToken(type); + } + + /** + * Gets type token for the given {@code Type} instance. + */ + public static TypeToken get(Type type) + { + return new SimpleTypeToken(type); + } + + private static class SimpleTypeToken extends TypeToken + { + public SimpleTypeToken(Type type) + { + super(type); + } + } + + @Override + public boolean equals(Object obj) + { + return (obj instanceof TypeToken) && type.equals(((TypeToken) obj).type); + } + + @Override + public int hashCode() + { + return type.hashCode(); + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/VarMap.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/VarMap.java new file mode 100644 index 00000000..102b8427 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/VarMap.java @@ -0,0 +1,94 @@ +/* + * Copied from Gentyref project http://code.google.com/p/gentyref/ + * Reformatted and moved to fit package structure + */ +package com.google.sitebricks.conversion.generics; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.HashMap; +import java.util.Map; + +/** + * Mapping between type variables and actual parameters. + * + * @author Wouter Coekaerts + */ +class VarMap +{ + private final Map, Type> map = new HashMap, Type>(); + + /** + * Creates an empty VarMap + */ + VarMap() + { + } + + void add(TypeVariable variable, Type value) + { + map.put(variable, value); + } + + void addAll(TypeVariable[] variables, Type[] values) + { + assert variables.length == values.length; + for (int i = 0; i < variables.length; i++) + { + map.put(variables[i], values[i]); + } + } + + VarMap(TypeVariable[] variables, Type[] values) + { + addAll(variables, values); + } + + Type map(Type type) + { + if (type instanceof Class) + { + return type; + } + else if (type instanceof TypeVariable) + { + assert map.containsKey(type); + return map.get(type); + } + else if (type instanceof ParameterizedType) + { + ParameterizedType pType = (ParameterizedType) type; + return new ParameterizedTypeImpl((Class) pType.getRawType(), map(pType + .getActualTypeArguments()), pType.getOwnerType() == null ? pType.getOwnerType() + : map(pType.getOwnerType())); + } + else if (type instanceof WildcardType) + { + WildcardType wType = (WildcardType) type; + return new WildcardTypeImpl(map(wType.getUpperBounds()), map(wType.getLowerBounds())); + } + else if (type instanceof GenericArrayType) + { + return GenericArrayTypeImpl.createArrayType(map(((GenericArrayType) type) + .getGenericComponentType())); + } + else + { + throw new RuntimeException("not implemented: mapping " + type.getClass() + " (" + type + + ")"); + } + } + + Type[] map(Type[] types) + { + Type[] result = new Type[types.length]; + for (int i = 0; i < types.length; i++) + { + result[i] = map(types[i]); + } + return result; + } +} diff --git a/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/WildcardTypeImpl.java b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/WildcardTypeImpl.java new file mode 100644 index 00000000..6e20fe64 --- /dev/null +++ b/sitebricks-converter/src/main/java/com/google/sitebricks/conversion/generics/WildcardTypeImpl.java @@ -0,0 +1,67 @@ +/* + * Copied from Gentyref project http://code.google.com/p/gentyref/ + * Reformatted and moved to fit package structure + */ +package com.google.sitebricks.conversion.generics; + +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.Arrays; + +class WildcardTypeImpl implements WildcardType +{ + private final Type[] upperBounds; + private final Type[] lowerBounds; + + public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) + { + if (upperBounds.length == 0) + throw new IllegalArgumentException( + "There must be at least one upper bound. For an unbound wildcard, the upper bound must be Object"); + this.upperBounds = upperBounds; + this.lowerBounds = lowerBounds; + } + + public Type[] getUpperBounds() + { + return upperBounds; + } + + public Type[] getLowerBounds() + { + return lowerBounds; + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof WildcardType)) + return false; + WildcardType other = (WildcardType) obj; + return Arrays.equals(lowerBounds, other.getLowerBounds()) + && Arrays.equals(upperBounds, other.getUpperBounds()); + } + + @Override + public int hashCode() + { + return Arrays.hashCode(lowerBounds) ^ Arrays.hashCode(upperBounds); + } + + @Override + public String toString() + { + if (lowerBounds.length > 0) + { + return "? super " + Generics.getTypeName(lowerBounds[0]); + } + else if (upperBounds[0] == Object.class) + { + return "?"; + } + else + { + return "? extends " + Generics.getTypeName(upperBounds[0]); + } + } +} diff --git a/sitebricks-converter/src/test/java/com/google/sitebricks/conversion/MvelTypeConverterTest.java b/sitebricks-converter/src/test/java/com/google/sitebricks/conversion/MvelTypeConverterTest.java new file mode 100644 index 00000000..21638623 --- /dev/null +++ b/sitebricks-converter/src/test/java/com/google/sitebricks/conversion/MvelTypeConverterTest.java @@ -0,0 +1,14 @@ +package com.google.sitebricks.conversion; + +import org.testng.annotations.Test; + +import com.google.sitebricks.conversion.MvelTypeConverter; + +public class MvelTypeConverterTest +{ + @Test + public void simpleConversions() { + MvelTypeConverter converter = new MvelTypeConverter(); + assert converter.convert(45, String.class).equals("45"); + } +} diff --git a/sitebricks-converter/src/test/java/com/google/sitebricks/conversion/StandardTypeConverterTest.java b/sitebricks-converter/src/test/java/com/google/sitebricks/conversion/StandardTypeConverterTest.java new file mode 100644 index 00000000..615959c8 --- /dev/null +++ b/sitebricks-converter/src/test/java/com/google/sitebricks/conversion/StandardTypeConverterTest.java @@ -0,0 +1,97 @@ +package com.google.sitebricks.conversion; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import com.google.inject.Binder; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.multibindings.Multibinder; +import com.google.sitebricks.conversion.DateConverters.DateStringConverter; + +/** + * @author JRodriguez + * @author John Patterson (jdpatterson@gmail.com) + */ +public class StandardTypeConverterTest { + + private TypeConverter converter; + + // a very weird date format + private String format = "ddd MM yy-EE a"; + + @BeforeTest + public void setup() { + + + Injector injector = Guice.createInjector(new Module() { + @Override + public void configure(Binder binder) { + // + // If the DateStringConverter is not added here first then the tests fail... + // There needs to be some way to override converters in a sane way. + // + Multibinder converters = Multibinder.newSetBinder(binder, Converter.class); + converters.addBinding().toInstance(new DateStringConverter(format)); + ConverterUtils.createConverterMultibinder(converters); + } + }); + + converter = injector.getInstance(StandardTypeConverter.class); + } + + @Test + public void stringToPrimitive() { + Integer answer = converter.convert("42", Integer.class); + assert answer == 42; + } + + @Test + public void numbers() { + BigDecimal answer = converter.convert(42, BigDecimal.class); + assert answer.intValue() == 42; + } + + @Test + public void dateToString() { + SimpleDateFormat sdf = new SimpleDateFormat (format); + Date date = new Date(); + String answer = converter.convert(date, String.class); + assert answer.equals(sdf.format(date)); + } + + @Test + public void stringToDate() { + SimpleDateFormat sdf = new SimpleDateFormat(format); + Date original = new Date(); + String expected = sdf.format(original); + Date converted = converter.convert(expected, Date.class); + String actual = sdf.format(converted); + assert actual.equals(expected); + } + + @Test + public void calendarToString() { + SimpleDateFormat sdf = new SimpleDateFormat(format); + Calendar calendar = Calendar.getInstance(); + String answer = converter.convert(calendar, String.class); + String expected = sdf.format(calendar.getTime()); + System.out.println( ">> " + answer ); + System.out.println( ">> " + expected ); + assert answer.equals(expected); + } + + @Test + public void stringToCalendar() { + SimpleDateFormat sdf = new SimpleDateFormat(format); + Calendar calendar = Calendar.getInstance(); + Calendar answer = converter.convert(sdf.format(calendar.getTime()), Calendar.class); + assert sdf.format(answer.getTime()).equals(sdf.format(calendar.getTime())); + } +} diff --git a/sitebricks-converter/src/test/java/com/google/sitebricks/conversion/TestTypeConverter.java b/sitebricks-converter/src/test/java/com/google/sitebricks/conversion/TestTypeConverter.java new file mode 100644 index 00000000..bb400a8c --- /dev/null +++ b/sitebricks-converter/src/test/java/com/google/sitebricks/conversion/TestTypeConverter.java @@ -0,0 +1,15 @@ +package com.google.sitebricks.conversion; + +import java.lang.reflect.Type; + +import com.google.sitebricks.conversion.TypeConverter; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class TestTypeConverter implements TypeConverter { + @SuppressWarnings("unchecked") + public T convert(Object raw, Type type) { + return (T) this; + } +} diff --git a/sitebricks-jetty-archetype/pom.xml b/sitebricks-jetty-archetype/pom.xml new file mode 100644 index 00000000..a5fbd730 --- /dev/null +++ b/sitebricks-jetty-archetype/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + com.google.sitebricks + sitebricks-parent + 0.8.5 + + sitebricks-jetty-archetype + Sitebricks :: Jetty Archetype + + + + com.google.sitebricks + sitebricks + + + org.mortbay.jetty + jetty + + + org.mortbay.jetty + jetty-util + + + org.mortbay.jetty + servlet-api-2.5 + + + ch.qos.logback + logback-classic + + + diff --git a/sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/AppConfig.java b/sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/AppConfig.java new file mode 100644 index 00000000..ba4ff157 --- /dev/null +++ b/sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/AppConfig.java @@ -0,0 +1,29 @@ +package info.sitebricks.example; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceServletContextListener; +import com.google.sitebricks.SitebricksModule; +import info.sitebricks.example.web.HomePage; + +/** + * The main configuration for a sitebricks servlet app. This class is typically + * registered as a <listener> in web.xml. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class AppConfig extends GuiceServletContextListener { + @Override + protected Injector getInjector() { + return Guice.createInjector(new SitebricksModule() { + @Override + protected void configureSitebricks() { + + // Tell Sitebricks to scan the contents of package info.sitebricks.example.web + // and ALL of its child packages for bricks, pages and other sitebricks artifacts. + scan(HomePage.class.getPackage()); + + } + }); + } +} diff --git a/sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/Main.java b/sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/Main.java new file mode 100644 index 00000000..6fd385e4 --- /dev/null +++ b/sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/Main.java @@ -0,0 +1,25 @@ +package info.sitebricks.example; + +import org.mortbay.jetty.Server; +import org.mortbay.jetty.webapp.WebAppContext; + +/** + * Main method kicks off the Jetty servlet container pointing at our Sitebricks webapp + * in source code. + *

+ * You should run this from the sitebricks-jetty-archetype directory or appropriately change + * the directory specified ("src/main/resources" by default) below. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class Main { + private static final int PORT = 8080; + + public static void main(String... args) throws Exception { + Server server = new Server(PORT); + server.addHandler(new WebAppContext("src/main/resources", "/")); + + server.start(); + server.join(); + } +} diff --git a/sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/web/HomePage.java b/sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/web/HomePage.java new file mode 100644 index 00000000..3d90c39d --- /dev/null +++ b/sitebricks-jetty-archetype/src/main/java/info/sitebricks/example/web/HomePage.java @@ -0,0 +1,32 @@ +package info.sitebricks.example.web; + +import com.google.sitebricks.At; +import com.google.sitebricks.Visible; +import com.google.sitebricks.http.Get; + +/** + * The home page that our users will see at the top level URI "/". + *

+ * This page is created once per request and has "no scope" in Guice + * terminology. See the Guice wiki + * for details. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@At("/") +public class HomePage { + + @Visible + String message; + + @Get + void showHome() { + // This is where you would normally fetch stuff from a database, for example. + message = "Hello from Sitebricks!"; + } + + public boolean getShouldShow() { + // Always show our message. + return true; + } +} diff --git a/sitebricks-jetty-archetype/src/main/resources/HomePage.html b/sitebricks-jetty-archetype/src/main/resources/HomePage.html new file mode 100644 index 00000000..47b111dc --- /dev/null +++ b/sitebricks-jetty-archetype/src/main/resources/HomePage.html @@ -0,0 +1,14 @@ + + + + Sitebricks :: Servlet/Jetty Archetype + + + + + @ShowIf(shouldShow) +

Message from the app: ${message}
+ + + + \ No newline at end of file diff --git a/sitebricks-jetty-archetype/src/main/resources/WEB-INF/web.xml b/sitebricks-jetty-archetype/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..99927f71 --- /dev/null +++ b/sitebricks-jetty-archetype/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,30 @@ + + + + + + webFilter + com.google.inject.servlet.GuiceFilter + + + + webFilter + /* + + + + info.sitebricks.example.AppConfig + + + diff --git a/sitebricks-mail/pom.xml b/sitebricks-mail/pom.xml new file mode 100644 index 00000000..def65551 --- /dev/null +++ b/sitebricks-mail/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + com.google.sitebricks + sitebricks-parent + 0.8.5 + + sitebricks-mail + Sitebricks :: Mail Client + + + + jboss-maven2-public-repository + http://repository.jboss.org/nexus/content/groups/public-jboss/ + + false + + + + + + + com.google.sitebricks + sitebricks + 0.8.5 + + + org.jboss.netty + netty + 3.2.4.Final + + + org.apache.james + james-server-imapserver + 3.0-M2 + + + org.slf4j + slf4j-api + 1.5.5 + + + ch.qos.logback + logback-classic + ${ch.qos.logback.version} + + + ch.qos.logback + logback-core + ${ch.qos.logback.version} + + + org.testng + testng + 5.8 + jdk15 + test + + + + + sitebricks-mail + + + src/test/resources + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/CommandCompletion.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/CommandCompletion.java new file mode 100644 index 00000000..a24f35ee --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/CommandCompletion.java @@ -0,0 +1,51 @@ +package com.google.sitebricks.mail; + +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.ValueFuture; +import com.google.sitebricks.mail.imap.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * A generic command completion listener that aggregates incoming messages + * until it forms a complete response to an issued command. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class CommandCompletion { + private static final Logger log = LoggerFactory.getLogger(CommandCompletion.class); + private final ValueFuture valueFuture; + private final List value = Lists.newArrayList(); + private final Long sequence; + private final Command command; + + @SuppressWarnings("unchecked") // Ugly gunk needed to prevent generics from spewing everywhere + public CommandCompletion(Command command, Long sequence, ValueFuture valueFuture) { + this.valueFuture = (ValueFuture) valueFuture; + this.sequence = sequence; + this.command = command; + } + + public boolean complete(String message) { + String[] pieces = message.split("[ ]+", 2); + + String status = pieces[1].toLowerCase(); + if (status.startsWith("ok") && status.contains("success")) { + // Ensure sequencing was correct. + if (!Long.valueOf(pieces[0]).equals(sequence)) { + log.error("Sequencing incorrect, expected {} but was {} ", sequence, pieces[0]); + } + + // Give it the final message and then process the data. + value.add(pieces[1]); + valueFuture.set(command.extract(value)); + return true; + } + + value.add(pieces[1]); + + return false; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/FolderObserver.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/FolderObserver.java new file mode 100644 index 00000000..50645d89 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/FolderObserver.java @@ -0,0 +1,21 @@ +package com.google.sitebricks.mail; + +/** + * Listens for IMAP folder events such as new mail arriving. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public interface FolderObserver { + /** + * New mail arrived in this folder. The client should now + * check for new MessageStatuses. + */ + void onMailAdded(); + + /** + * Existing mail was expunged from this folder. This could + * happen via other clients or the activation of server-side + * filters for example. + */ + void onMailRemoved(); +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/Mail.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/Mail.java new file mode 100644 index 00000000..9ab3e7c5 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/Mail.java @@ -0,0 +1,24 @@ +package com.google.sitebricks.mail; + +import com.google.inject.ImplementedBy; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@ImplementedBy(SitebricksMail.class) +public interface Mail { + AuthBuilder clientOf(String host, int port); + + public enum Auth { PLAIN, SSL, OAUTH } + + public static interface AuthBuilder { + AuthBuilder timeout(long amount, TimeUnit unit); + + AuthBuilder executors(ExecutorService bossPool, ExecutorService workerPool); + + MailClient connect(Auth authType, String username, String password); + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClient.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClient.java new file mode 100644 index 00000000..bcc76a0f --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClient.java @@ -0,0 +1,87 @@ +package com.google.sitebricks.mail; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.sitebricks.mail.imap.Folder; +import com.google.sitebricks.mail.imap.FolderStatus; +import com.google.sitebricks.mail.imap.Message; +import com.google.sitebricks.mail.imap.MessageStatus; + +import java.util.List; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public interface MailClient { + /** + * Connects to the IMAP server logs in with the given credentials. + */ + void connect(); + + /** + * Logs out of the current IMAP session and releases all resources, including + * executor services. + */ + void disconnect(); + + List capabilities(); + + ListenableFuture> listFolders(); + + ListenableFuture statusOf(String folder); + + /** + * Opens a logical 'session' to the given folder name. This method must be called + * prior to using many of the in-folder methods on this API. + */ + ListenableFuture open(String folder); + + /** + * Returns a list of message headers in the given folder, between {@code start} + * and {@code end}. The start is a 1-based index into the current session's "view" + * of an IMAP folder. The message numbers are guaranteed not to change UNLESS + * new mail is added or removed during a session (listen for this using the + * {@link #watch(Folder, FolderObserver)} method. + *

+ * The returned list is in ascending order (start : end) determined by the IMAP + * server (the protocol requires chronological descending order--recent : old). + *

+ * The messages returned in the list are lightweight {@link MessageStatus} objects + * which only contain cursory information and some metadata about a message, + * including the subject. This is useful to quickly obtain a list of messages + * whose bodies can be fetched later with the more comprehensive + * {@link #fetch(Folder, int, int)} method. + *

+ * NOTE: you must call {@link #open(String)} first. + */ + ListenableFuture> list(Folder folder, int start, int end); + + /** + * Similar to {@link #list(Folder, int, int)} but fetches the entire message + * instead of merely a header. Runs a bit slower as a result. + *

+ * This returns the complete details of an email message. Prefer {@link #list(Folder, int, int)} + * for fetching just subjects/status info as this method can be slower + * for messages with large bodies. + *

+ * NOTE: you must call {@link #open(String)} first. + */ + public ListenableFuture> fetch(Folder folder, int start, int end); + + /** + * + *

+ * NOTE: you must call {@link #open(String)} first. + */ + void watch(Folder folder, FolderObserver observer); + + /** + * Stops watching a folder if one was currently being watched (otherwise + * a noop). Events from 'IDLEing' will immediately stop when this method + * returns and the registered {@link FolderObserver} will be forgotten. + *

+ * Note the subtle point that this method (though it doesn't block) will + * immediately stop firing events to its FolderObserver. This happens + * even before IDLEing ceases on the server. + */ + void unwatch(); +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientConfig.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientConfig.java new file mode 100644 index 00000000..7dae78d1 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientConfig.java @@ -0,0 +1,49 @@ +package com.google.sitebricks.mail; + +import com.google.sitebricks.mail.Mail.Auth; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class MailClientConfig { + private final String host; + private final int port; + private final Auth authType; + private final String username; + private final String password; + private final long timeout; + + public MailClientConfig(String host, int port, Auth authType, String username, String password, + long timeout) { + this.host = host; + this.port = port; + this.authType = authType; + this.username = username; + this.password = password; + this.timeout = timeout; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public Auth getAuthType() { + return authType; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public long getTimeout() { + return timeout; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientHandler.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientHandler.java new file mode 100644 index 00000000..7ac467f8 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientHandler.java @@ -0,0 +1,109 @@ +package com.google.sitebricks.mail; + +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; + +/** + * A command/response handler for a single mail connection/user. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class MailClientHandler extends SimpleChannelHandler { + private static final Logger log = LoggerFactory.getLogger(MailClientHandler.class); + public static final String CAPABILITY_PREFIX = "* CAPABILITY"; + + private final CountDownLatch loginComplete = new CountDownLatch(2); + private volatile boolean isLoggedIn = false; + private volatile List capabilities; + private volatile FolderObserver observer; + + private final Queue completions = new ConcurrentLinkedQueue(); + + public void enqueue(Long sequence, CommandCompletion completion) { + completions.add(completion); + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + String message = e.getMessage().toString(); + log.debug("Message received [{}] from {}", e.getMessage(), e.getRemoteAddress()); + + if (message.startsWith(CAPABILITY_PREFIX)) { + this.capabilities = Arrays.asList( + message.substring(CAPABILITY_PREFIX.length() + 1).split("[ ]+")); + loginComplete.countDown(); + return; + } + + if (!isLoggedIn) { + if (message.matches("[.] OK .*@.* \\(Success\\)")) { + log.debug("Authentication success."); + isLoggedIn = true; + loginComplete.countDown(); + } + // TODO handle auth failed + return; + } + + if (null != observer) { + message = message.toLowerCase(); + if (message.endsWith("exists")) { + observer.onMailAdded(); + return; + } else if (message.endsWith("expunge")) { + observer.onMailRemoved(); + return; + } + } + + complete(message); + } + + private void complete(String message) { + CommandCompletion completion = completions.peek(); + if (completion == null) { + log.error("Could not find the completion for message {} (Was it ever issued?)", message); + return; + } + + if (completion.complete(message)) { + completions.poll(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { + log.error("Exception caught!", e.getCause()); + } + + public List getCapabilities() { + return capabilities; + } + + void awaitLogin() { + try { + loginComplete.await(); + } catch (InterruptedException e) { + throw new RuntimeException("Interruption while awaiting server login", e); + } + } + + /** + * Registers a FolderObserver to receive events happening with a particular + * folder. Typically an IMAP IDLE feature. If called multiple times, will + * overwrite the currently set observer. + */ + void observe(FolderObserver observer) { + this.observer = observer; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientPipelineFactory.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientPipelineFactory.java new file mode 100644 index 00000000..d142b0e2 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientPipelineFactory.java @@ -0,0 +1,50 @@ +package com.google.sitebricks.mail; + +import com.google.sitebricks.mail.Mail.Auth; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder; +import org.jboss.netty.handler.codec.frame.Delimiters; +import org.jboss.netty.handler.codec.string.StringDecoder; +import org.jboss.netty.handler.codec.string.StringEncoder; +import org.jboss.netty.handler.ssl.SslHandler; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class MailClientPipelineFactory implements ChannelPipelineFactory { + private final MailClientHandler mailClientHandler; + private final MailClientConfig config; + + public MailClientPipelineFactory(MailClientHandler mailClientHandler, MailClientConfig config) { + this.mailClientHandler = mailClientHandler; + this.config = config; + } + + public ChannelPipeline getPipeline() throws Exception { + // Create a default pipeline implementation. + ChannelPipeline pipeline = Channels.pipeline(); + + if (config.getAuthType() == Auth.SSL) { + SSLEngine sslEngine = SSLContext.getDefault().createSSLEngine(); + sslEngine.setUseClientMode(true); + SslHandler sslHandler = new SslHandler(sslEngine); + sslHandler.setEnableRenegotiation(true); + pipeline.addLast("ssl", sslHandler); + } + + // Add the text line codec combination first, + pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); + pipeline.addLast("decoder", new StringDecoder()); + pipeline.addLast("encoder", new StringEncoder()); + + // and then business logic. + pipeline.addLast("handler", mailClientHandler); + + return pipeline; + } +} \ No newline at end of file diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/NettyImapClient.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/NettyImapClient.java new file mode 100644 index 00000000..989532f4 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/NettyImapClient.java @@ -0,0 +1,205 @@ +package com.google.sitebricks.mail; + +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ValueFuture; +import com.google.sitebricks.mail.imap.*; +import org.jboss.netty.bootstrap.ClientBootstrap; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class NettyImapClient implements MailClient { + private static final Logger log = LoggerFactory.getLogger(NettyImapClient.class); + + private final ExecutorService workerPool; + private final ClientBootstrap bootstrap; + + private final MailClientConfig config; + private final MailClientHandler mailClientHandler; + + private volatile Channel channel; + private final AtomicLong sequence = new AtomicLong(); + + private volatile Folder currentFolder = null; + + public NettyImapClient(MailClientPipelineFactory pipelineFactory, + MailClientConfig config, + MailClientHandler mailClientHandler, + ExecutorService bossPool, + ExecutorService workerPool) { + this.workerPool = workerPool; + this.bootstrap = new ClientBootstrap(new NioClientSocketChannelFactory( + bossPool, + workerPool)); + + this.config = config; + this.mailClientHandler = mailClientHandler; + this.bootstrap.setPipelineFactory(pipelineFactory); + } + + /** + * Connects to the IMAP server logs in with the given credentials. + */ + @Override + public void connect() { + ChannelFuture future = bootstrap.connect(new InetSocketAddress(config.getHost(), + config.getPort())); + + Channel channel = future.awaitUninterruptibly().getChannel(); + if (!future.isSuccess()) { + bootstrap.releaseExternalResources(); + throw new RuntimeException("Could not connect channel", future.getCause()); + } + + this.channel = channel; + login(); + } + + private void login() { + channel.write(". CAPABILITY\r\n"); + channel.write(". login " + config.getUsername() + " " + config.getPassword() + "\r\n"); + mailClientHandler.awaitLogin(); + } + + /** + * Logs out of the current IMAP session and releases all resources, including + * executor services. + */ + @Override + public void disconnect() { + currentFolder = null; + + // Log out of the IMAP Server. + channel.write(". logout\n"); + + // Shut down all thread pools and exit. + channel.close().awaitUninterruptibly(config.getTimeout(), TimeUnit.MILLISECONDS); + bootstrap.releaseExternalResources(); + + // TODO Shut down thread pools? + } + + ChannelFuture send(Command command, String args, ValueFuture valueFuture) { + Long seq = sequence.incrementAndGet(); + + String commandString = seq + " " + command.toString() + + (null == args ? "" : " " + args) + + "\r\n"; + log.debug("Sending {} to server...", commandString); + + // Enqueue command. + mailClientHandler.enqueue(seq, new CommandCompletion(command, seq, valueFuture)); + + return channel.write(commandString); + } + + @Override + public List capabilities() { + return mailClientHandler.getCapabilities(); + } + + @Override + public ListenableFuture> listFolders() { + ValueFuture> valueFuture = ValueFuture.create(); + + send(Command.LIST_FOLDERS, "\"[Gmail]\" \"*\"", valueFuture); + + return valueFuture; + } + + @Override + public ListenableFuture statusOf(String folder) { + ValueFuture valueFuture = ValueFuture.create(); + + String args = '"' + folder + "\" (UIDNEXT RECENT MESSAGES UNSEEN)"; + send(Command.FOLDER_STATUS, args, valueFuture); + + return valueFuture; + } + + @Override + public ListenableFuture open(String folder) { + final ValueFuture valueFuture = ValueFuture.create(); + valueFuture.addListener(new Runnable() { + @Override + public void run() { + try { + currentFolder = valueFuture.get(); + } catch (InterruptedException e) { + log.error("Interrupted while attempting to open a folder", e); + } catch (ExecutionException e) { + log.error("Execution exception while attempting to open a folder", e); + } + } + }, workerPool); + + String args = '"' + folder + "\""; + send(Command.FOLDER_OPEN, args, valueFuture); + + return valueFuture; + } + + @Override + public ListenableFuture> list(Folder folder, int start, int end) { + checkCurrentFolder(folder); + Preconditions.checkArgument(start <= end, "Start must be <= end"); + Preconditions.checkArgument(start > 0, "Start must be greater than zero (IMAP uses 1-based " + + "indexing)"); + ValueFuture> valueFuture = ValueFuture.create(); + + String args = start + ":" + end + " all"; + send(Command.FETCH_HEADERS, args, valueFuture); + + return valueFuture; + } + + @Override + public ListenableFuture> fetch(Folder folder, int start, int end) { + checkCurrentFolder(folder); + Preconditions.checkArgument(start <= end, "Start must be <= end"); + Preconditions.checkArgument(start > 0, "Start must be greater than zero (IMAP uses 1-based " + + "indexing)"); + ValueFuture> valueFuture = ValueFuture.create(); + + String args = start + ":" + end + " all"; + send(Command.FETCH_FULL, args, valueFuture); + + return valueFuture; + } + + @Override + public void watch(Folder folder, FolderObserver observer) { + checkCurrentFolder(folder); + + send(Command.IDLE, null, ValueFuture.create()); + + mailClientHandler.observe(observer); + } + + @Override + public void unwatch() { + // Stop watching folders. + mailClientHandler.observe(null); + + channel.write(". DONE"); + } + + private void checkCurrentFolder(Folder folder) { + Preconditions.checkState(folder.equals(currentFolder), "You must have opened folder %s" + + " before attempting to read from it (%s is currently open).", folder.getName(), + (currentFolder == null ? "No folder" : currentFolder.getName())); + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/SitebricksMail.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/SitebricksMail.java new file mode 100644 index 00000000..09873868 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/SitebricksMail.java @@ -0,0 +1,65 @@ +package com.google.sitebricks.mail; + +import com.google.common.base.Preconditions; +import com.google.sitebricks.mail.Mail.AuthBuilder; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class SitebricksMail implements Mail, AuthBuilder { + private String host; + private int port; + + private long timeout; + + private ExecutorService bossPool; + private ExecutorService workerPool; + + @Override + public AuthBuilder clientOf(String host, int port) { + Preconditions.checkArgument(null != host && !host.isEmpty(), + "Must specify a valid hostname"); + Preconditions.checkArgument(port > 0, + "Must specify a valid (non-zero) port"); + this.host = host; + this.port = port; + return this; + } + + @Override + public AuthBuilder timeout(long amount, TimeUnit unit) { + this.timeout = unit.convert(amount, TimeUnit.MILLISECONDS); + return this; + } + + @Override + public AuthBuilder executors(ExecutorService bossPool, ExecutorService workerPool) { + Preconditions.checkArgument(bossPool != null, "Boss executor cannot be null!"); + Preconditions.checkArgument(workerPool != null, "Worker executor cannot be null!"); + this.bossPool = bossPool; + this.workerPool = workerPool; + return this; + } + + @Override + public MailClient connect(Auth authType, String username, String password) { + if (null == bossPool) { + bossPool = Executors.newCachedThreadPool(); + workerPool = Executors.newCachedThreadPool(); + } + + MailClientConfig config = new MailClientConfig(host, port, authType, username, + password, timeout); + MailClientHandler mailClientHandler = new MailClientHandler(); + MailClient client = new NettyImapClient(new MailClientPipelineFactory(mailClientHandler, + config), config, mailClientHandler, bossPool, workerPool); + + // Blocks until connected (timeout specified in config). + client.connect(); + return client; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/example b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/example new file mode 100644 index 00000000..87ff1e9b --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/example @@ -0,0 +1,20 @@ +("Fri, 8 Apr 2011 23:12:09 -0700" + "Get Gmail on your mobile phone" + (("Gmail Team" NIL "mail-noreply" "google.com")) from + (("Gmail Team" NIL "mail-noreply" "google.com")) sender + (("Gmail Team" NIL "mail-noreply" "google.com")) reply-to + (("imap test" NIL "telnet.imap" "gmail.com")) to + NIL NIL NIL cc/bcc? + "") + + +("Tue, 26 Apr 2011 23:02:08 +1000" + "Fwd: test push" + (("Dhanji R. Prasanna" NIL "dhanji" "gmail.com")) from + (("Dhanji R. Prasanna" NIL "dhanji" "gmail.com")) sender + (("Dhanji R. Prasanna" NIL "dhanji" "gmail.com")) reply-to + ((NIL NIL "telnet.imap" "gmail.com")) to + NIL cc + NIL bcc + "" in-reply-to + "") diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Command.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Command.java new file mode 100644 index 00000000..a75d621d --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Command.java @@ -0,0 +1,43 @@ +package com.google.sitebricks.mail.imap; + +import com.google.common.collect.Maps; + +import java.util.List; +import java.util.Map; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public enum Command { + LIST_FOLDERS("list"), + FETCH_FULL("fetch"), + FOLDER_STATUS("status"), + FOLDER_OPEN("select"), + FETCH_HEADERS("fetch"), + IDLE("idle"); // IMAP4 IDLE command: http://www.ietf.org/rfc/rfc2177.txt + + private final String commandString; + private Command(String commandString) { + this.commandString = commandString; + } + + private static final Map> dataExtractors; + static { + dataExtractors = Maps.newHashMap(); + + dataExtractors.put(LIST_FOLDERS, new ListFoldersExtractor()); + dataExtractors.put(FOLDER_STATUS, new FolderStatusExtractor()); + dataExtractors.put(FOLDER_OPEN, new FolderExtractor()); + dataExtractors.put(FETCH_HEADERS, new MessageStatusExtractor()); + } + + @SuppressWarnings("unchecked") // Heterogenous collections are a pita in Java. + public D extract(List message) { + return (D) dataExtractors.get(this).extract(message); + } + + @Override + public String toString() { + return commandString; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Extractor.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Extractor.java new file mode 100644 index 00000000..a9c3c6a1 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Extractor.java @@ -0,0 +1,12 @@ +package com.google.sitebricks.mail.imap; + +import java.util.List; + +/** + * A command utility that extracts data for particular commands. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +interface Extractor { + D extract(List messages); +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Flag.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Flag.java new file mode 100644 index 00000000..85cb828e --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Flag.java @@ -0,0 +1,18 @@ +package com.google.sitebricks.mail.imap; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public enum Flag { + SEEN, + RECENT; + + public static Flag named(String name) { + if ("SEEN".equals(name)) { + return SEEN; + } else if ("RECENT".equals(name)) { + return RECENT; + } + return null; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Folder.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Folder.java new file mode 100644 index 00000000..c2d2e18e --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Folder.java @@ -0,0 +1,27 @@ +package com.google.sitebricks.mail.imap; + +/** + * Simple data object that represents an IMAP folder. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class Folder { + private final String name; + private int count; + + public Folder(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public int getCount() { + return count; + } + + void setCount(int count) { + this.count = count; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderExtractor.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderExtractor.java new file mode 100644 index 00000000..4d0b41f5 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderExtractor.java @@ -0,0 +1,36 @@ +package com.google.sitebricks.mail.imap; + +import com.google.common.base.Preconditions; + +import java.util.List; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class FolderExtractor implements Extractor { + + private static final String SELECTED = "selected"; + + @Override + public Folder extract(List messages) { + String folderName = null; + int count = 0; + for (String message : messages) { + String[] pieces = message.split("[ ]+", 3); + if (pieces.length > 1 && "EXISTS".equalsIgnoreCase(pieces[1])) { + count = Integer.valueOf(pieces[0]); + } else if (message.contains(SELECTED)) { + // Extract folder name as given by the server. + int left = message.indexOf(pieces[1]) + pieces[1].length(); + folderName = message.substring(left, message.indexOf(SELECTED)).trim(); + } + } + + Preconditions.checkState(null != folderName, "Error in IMAP protocol, " + + "could not detect folder name"); + + Folder folder = new Folder(folderName); + folder.setCount(count); + return folder; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderStatus.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderStatus.java new file mode 100644 index 00000000..060eabca --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderStatus.java @@ -0,0 +1,55 @@ +package com.google.sitebricks.mail.imap; + +/** + * Some metadata about an IMAP folder. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class FolderStatus { + private int messages; + private int unseen; + private int recent; + private int nextUid; + + void setMessages(int messages) { + this.messages = messages; + } + + void setUnseen(int unseen) { + this.unseen = unseen; + } + + void setRecent(int recent) { + this.recent = recent; + } + + void setNextUid(int nextUid) { + this.nextUid = nextUid; + } + + public int getMessages() { + return messages; + } + + public int getUnseen() { + return unseen; + } + + public int getRecent() { + return recent; + } + + public int getNextUid() { + return nextUid; + } + + @Override + public String toString() { + return "FolderStatus{" + + "messages=" + messages + + ", unseen=" + unseen + + ", recent=" + recent + + ", nextUid=" + nextUid + + '}'; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderStatusExtractor.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderStatusExtractor.java new file mode 100644 index 00000000..836205ff --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/FolderStatusExtractor.java @@ -0,0 +1,42 @@ +package com.google.sitebricks.mail.imap; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class FolderStatusExtractor implements Extractor { + private static final Pattern PARENS = Pattern.compile("([(].*[)])"); + + @Override + public FolderStatus extract(List messages) { + FolderStatus status = new FolderStatus(); + + // There should generally only be 1. + for (String message : messages) { + Matcher matcher = PARENS.matcher(message); + if (matcher.find()) { + String group = matcher.group(1); + + // Strip parens. + group = group.substring(1, group.length() - 1); + String[] pieces = group.split("[ ]+"); + for (int i = 0; i < pieces.length; i += 2) { + String piece = pieces[i]; + if ("MESSAGES".equalsIgnoreCase(piece)) { + status.setMessages(Integer.valueOf(pieces[i + 1])); + } else if ("UNSEEN".equalsIgnoreCase(piece)) { + status.setUnseen(Integer.valueOf(pieces[i + 1])); + } else if ("RECENT".equalsIgnoreCase(piece)) { + status.setRecent(Integer.valueOf(pieces[i + 1])); + } else if ("UIDNEXT".equalsIgnoreCase(piece)) { + status.setNextUid(Integer.valueOf(pieces[i + 1])); + } + } + } + } + return status; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/ListFoldersExtractor.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/ListFoldersExtractor.java new file mode 100644 index 00000000..f9355244 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/ListFoldersExtractor.java @@ -0,0 +1,40 @@ +package com.google.sitebricks.mail.imap; + +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class ListFoldersExtractor implements Extractor> { + private static final Pattern QUOTES = Pattern.compile("(\".*\")"); + private static final String ROOT_PREFIX = "\"/\""; + + @Override + public List extract(List messages) { + ImmutableList.Builder builder = ImmutableList.builder(); + for (String message : messages) { + Matcher matcher = QUOTES.matcher(message); + if (matcher.find()) { + String group = matcher.group(1); + + if (group.startsWith(ROOT_PREFIX)) { + group = group.substring(ROOT_PREFIX.length()).trim(); + } + + // Strip quotes. + if (group.startsWith("\"")) { + group = group.substring(1, group.length() - 1); + } + + // Generally remove leading "/" and stripquotes + builder.add(group); + } + } + + return builder.build(); + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Message.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Message.java new file mode 100644 index 00000000..1c6a0d85 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/Message.java @@ -0,0 +1,11 @@ +package com.google.sitebricks.mail.imap; + +/** + * Represents a complete IMAP message with all body parts materialized + * and decoded as appropriate (for example, non-UTF8 encodings are re-encoded + * into UTF8 for raw and rich text). + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class Message { +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageExtractor.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageExtractor.java new file mode 100644 index 00000000..c9e9b71e --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageExtractor.java @@ -0,0 +1,69 @@ +package com.google.sitebricks.mail.imap; + +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * Extracts a Message from a complete IMAP fetch. Specifically + * a "fetch full" command which comes back with subject, sender, uid + * internaldate and rfc822.size (length) and all body parts. + *

+ * This is the more robust form of {@link MessageStatus}, which should + * be preferred for fetching just subjects/status info as this extraction + * mechanism can be slower for messages with large bodies. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class MessageExtractor implements Extractor> { + private static final String ENVELOPE_PREFIX = "(ENVELOPE "; + private static final String INTERNALDATE = "INTERNALDATE"; + + @Override + public List extract(List messages) { + List statuses = Lists.newArrayList(); + for (String message : messages) { + System.out.println(message); + String[] split = message.split("[ ]+", 3); + + // Only parse Fetch responses. + if (split.length > 1 && "FETCH".equalsIgnoreCase(split[1])) { + // Strip the "XX FETCH" sequence prefix first. +// statuses.add(parseEnvelope(split[2])); + } + } + + return statuses; + } + + + private static List tokenize(String message) { + List pieces = Lists.newArrayList(); + char[] chars = message.toCharArray(); + boolean inString = false; + StringBuilder token = new StringBuilder(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (c == '"') { + + // Close of string, bake this token. + if (inString) { + pieces.add(token.toString().trim()); + token = new StringBuilder(); + inString = false; + } else + inString = true; + + continue; + } + + // Skip parentheticals + if (!inString && (c == '(' || c == ')')) { + continue; + } + + token.append(c); + } + return pieces; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageStatus.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageStatus.java new file mode 100644 index 00000000..bec7a006 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageStatus.java @@ -0,0 +1,71 @@ +package com.google.sitebricks.mail.imap; + +import java.util.Date; +import java.util.EnumSet; + +/** + * Represents a single email message. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class MessageStatus { + private final String messageUid; + private final Date receivedDate; + private final Date internalDate; + private final String subject; + private final EnumSet flags; + + private final String from; + private final String sender; + private final String replyTo; + + public MessageStatus(String messageUid, + Date receivedDate, + Date internalDate, + String subject, + EnumSet flags, + String from, String sender, String replyTo) { + this.messageUid = messageUid; + this.receivedDate = receivedDate; + this.internalDate = internalDate; + this.subject = subject; + this.flags = flags; + this.from = from; + this.sender = sender; + this.replyTo = replyTo; + } + + public String getMessageUid() { + return messageUid; + } + + public Date getReceivedDate() { + return receivedDate; + } + + public Date getInternalDate() { + return internalDate; + } + + public String getSubject() { + return subject; + } + + public EnumSet getFlags() { + return flags; + } + + @Override + public String toString() { + return "MessageStatus{" + + "messageUid='" + messageUid + '\'' + + ", receivedDate=" + receivedDate + + ", internalDate=" + internalDate + + ", subject='" + subject + '\'' + + ", flags=" + flags + + ", from='" + from + '\'' + + ", sender='" + sender + '\'' + + ", replyTo='" + replyTo + '\'' + + '}'; + } +} diff --git a/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageStatusExtractor.java b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageStatusExtractor.java new file mode 100644 index 00000000..c1dee0a6 --- /dev/null +++ b/sitebricks-mail/src/main/java/com/google/sitebricks/mail/imap/MessageStatusExtractor.java @@ -0,0 +1,202 @@ +package com.google.sitebricks.mail.imap; + +import com.google.common.collect.Lists; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; + +/** + * Extracts a MessageStatus from a partial IMAP fetch. Specifically + * a "fetch all" command which comes back with subject, sender, uid + * internaldate and rfc822.size (length). + *

+ * A more robust form of fetch exists for message body parts which + * would handle email body, html mail, attachments, etc. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class MessageStatusExtractor implements Extractor> { + private static final String ENVELOPE_PREFIX = "(ENVELOPE "; + private static final String INTERNALDATE = "INTERNALDATE"; + + static final String RECEIVED_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss ZZZZZ"; + static final String INTERNAL_DATE_FORMAT = "dd-MMM-yyyy HH:mm:ss ZZZZZ"; + + @Override + public List extract(List messages) { + List statuses = Lists.newArrayList(); + for (String message : messages) { + String[] split = message.split("[ ]+", 3); + + // Only parse Fetch responses. + if (split.length > 1 && "FETCH".equalsIgnoreCase(split[1])) { + // Strip the "XX FETCH" sequence prefix first. + statuses.add(parseEnvelope(split[2])); + } + } + + return statuses; + } + + private static MessageStatus parseEnvelope(String message) { + // Now we have only the envelope remaining. + if (!message.startsWith(ENVELOPE_PREFIX)) { + // Something's wrong, we can't handle this. + throw new RuntimeException("Illegal data format, expecting envelope prefix, " + + "found " + message); + } + + // Strip envelope wrapper. + message = message.substring(ENVELOPE_PREFIX.length(), message.length() - 1); + + // Parse strings or paren-groups. + List tokens = tokenize(message); + + // Parse semantic message information out of the token stream. + + // First piece is always the received date. + String receivedDateRaw = tokens.get(0); + String subject = tokens.get(1); + + // sender/recipient etc. are parsed as 4-part address structures. + String from = parseAddress(tokens, 2); + String sender = parseAddress(tokens, 3); + String replyTo = parseAddress(tokens, 4); + String to = parseAddress(tokens, 5); // TODO handle multiple recipients. + + + // TODO Should we reimplement the parser to split these NIL tokens? Yes, I think so. + System.out.println(tokens.get(6)); + + // Skip ahead to last-but-one (message uid). + String messageUidRaw = tokens.get(tokens.size() - 2); + String messageUid = messageUidRaw.substring(messageUidRaw.indexOf('<') + 1, + messageUidRaw.length() - 1); + + // Last token is the combined Flags and internaldate token. + EnumSet flags = EnumSet.noneOf(Flag.class); + String last = tokens.get(tokens.size() - 1); + for (String fragment : last.split("[ ]+")) { + if (fragment.startsWith("\\")) { + // This is an IMAP flag. Do something with it. + Flag flag = Flag.named(fragment.substring(1).toUpperCase()); + if (null != flag) + flags.add(flag); + // Else maybe log warning that we encountered an unknown flag? + } + + // Are we done processing flags? + if (INTERNALDATE.equalsIgnoreCase(fragment)) { + break; + } + } + + last = last.substring(last.indexOf(INTERNALDATE) + INTERNALDATE.length() + 1); + + // Parse date from this final piece. + Date internalDate, receivedDate; + try { + SimpleDateFormat dateFormat = new SimpleDateFormat(INTERNAL_DATE_FORMAT); + internalDate = dateFormat.parse(last); + dateFormat = new SimpleDateFormat(RECEIVED_DATE_FORMAT); + receivedDate = dateFormat.parse(receivedDateRaw); + + } catch (ParseException e) { + throw new RuntimeException("Unable to parse date from " + receivedDateRaw, e); + } + + return new MessageStatus(messageUid, receivedDate, internalDate, subject, flags, from, sender, + replyTo); + } + + private static String parseAddress(List tokens, int start) { + StringBuilder builder = new StringBuilder(); + tokens = tokenize(tokens.get(start)); + start = 0; + + // Name of addressee. + String token = tokens.get(start); + if (isValid(token)) { + builder.append('"'); + builder.append(token); + builder.append("\" "); + } + + // Build the email address itself. + // TODO: Im not really sure what the start + 1 field is supposed to be (see addresses RFC). + token = tokens.get(start + 1); + String[] pieces = token.split("[ ]+"); + + // Strip out any NIL components and rebuild the email address token. + StringBuilder smaller = new StringBuilder(); + for (String piece : pieces) { + if (isValid(piece)) + smaller.append(piece); + } + token = smaller.toString(); + + builder.append('<'); + builder.append(token); + builder.append('@'); + builder.append(tokens.get(start + 2)); + builder.append('>'); + + return builder.toString(); + } + + private static boolean isValid(String token) { + return !"NIL".equalsIgnoreCase(token); + } + + private static List tokenize(String message) { + List pieces = Lists.newArrayList(); + char[] chars = message.toCharArray(); + boolean inString = false; + int paren = 0; + StringBuilder token = new StringBuilder(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + + // Skip top-level parentheticals, but honor 2nd-level ones. + if (!inString) { + if (c == '(') { + paren++; +// if (paren < 2) { // Skip 2 levels + continue; +// } + + } else if (c == ')') { + paren--; + + // Time to bake this as a token (skip top level). + if (paren > 1) { +// token.append(c); + pieces.add(token.toString().trim()); + token = new StringBuilder(); + } + continue; + } + } + + if (c == '"' && paren < 2) { + + // Close of string, bake this token. + if (inString) { + pieces.add(token.toString().trim()); + token = new StringBuilder(); + inString = false; + } else + inString = true; + + continue; + } + + token.append(c); + } + + return pieces; + } +} diff --git a/sitebricks-mail/src/test/java/com/google/sitebricks/mail/MailClientIntegrationTest.java b/sitebricks-mail/src/test/java/com/google/sitebricks/mail/MailClientIntegrationTest.java new file mode 100644 index 00000000..f9370d20 --- /dev/null +++ b/sitebricks-mail/src/test/java/com/google/sitebricks/mail/MailClientIntegrationTest.java @@ -0,0 +1,62 @@ +package com.google.sitebricks.mail; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.inject.Guice; +import com.google.sitebricks.mail.Mail.Auth; +import com.google.sitebricks.mail.imap.Folder; +import com.google.sitebricks.mail.imap.MessageStatus; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class MailClientIntegrationTest { + public static void main(String...args) throws InterruptedException, ExecutionException { + Mail mail = Guice.createInjector().getInstance(Mail.class); + + final MailClient client = mail.clientOf("imap.gmail.com", 993) + .connect(Auth.SSL, "telnet.imap@gmail.com", System.getProperty("sitebricks-mail.password")); + + List capabilities = client.capabilities(); + System.out.println("CAPS: " + capabilities); + + client.statusOf("[Gmail]/All Mail"); + ListenableFuture future = client.open("[Gmail]/All Mail"); + final Folder allMail = future.get(); + System.out.println("Folder opened: " + allMail.getName() + " with count " + allMail.getCount()); + + future.addListener(new Runnable() { + @Override + public void run() { +// client.watch(allMail, new FolderObserver() { +// @Override +// public void onMailAdded() { +// System.out.println("New mail arrived!!"); +// } +// +// @Override +// public void onMailRemoved() { +// System.out.println("Old mail removed!!"); +// } +// }); + + ListenableFuture> messages = client.list(allMail, 1, allMail.getCount()); + try { + System.out.println("Fetched: " + messages.get()); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + client.disconnect(); + + System.exit(0); + } + }, Executors.newCachedThreadPool()); + + + } +} diff --git a/sitebricks-mail/src/test/java/com/google/sitebricks/mail/imap/MessageStatusExtractorTest.java b/sitebricks-mail/src/test/java/com/google/sitebricks/mail/imap/MessageStatusExtractorTest.java new file mode 100644 index 00000000..73486b75 --- /dev/null +++ b/sitebricks-mail/src/test/java/com/google/sitebricks/mail/imap/MessageStatusExtractorTest.java @@ -0,0 +1,45 @@ +package com.google.sitebricks.mail.imap; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.EnumSet; +import java.util.List; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class MessageStatusExtractorTest { + + /** + * WARNING: THIS TEST IS DATA-DEPENDENT! + */ + @Test + public final void testTypicalGmailInboxHeaders() throws IOException, ParseException { + List lines = + Resources.readLines(MessageStatusExtractorTest.class.getResource("fetch_all_data.txt"), + Charsets.UTF_8); + + List statuses = new MessageStatusExtractor().extract(lines.subList(0, 1)); + + MessageStatus status = statuses.get(0); + assert EnumSet.noneOf(Flag.class).equals(status.getFlags()); + assert "BANLkTi=zC_UQExUuaNqiP0dJXoswDej1Ww@mail.gmail.com".equals(status.getMessageUid()); + assert new SimpleDateFormat(MessageStatusExtractor.RECEIVED_DATE_FORMAT) + .parse("Fri, 8 Apr 2011 23:12:09 -0700") + .equals(status.getReceivedDate()); + assert new SimpleDateFormat(MessageStatusExtractor.INTERNAL_DATE_FORMAT) + .parse("09-Apr-2011 06:12:09 +0000") + .equals(status.getInternalDate()); + assert "Get Gmail on your mobile phone".equals(status.getSubject()); + + for (MessageStatus st : statuses) { + System.out.println(st); + } + } + +} diff --git a/sitebricks-mail/src/test/java/com/google/sitebricks/mail/test/integration/TestImap.java b/sitebricks-mail/src/test/java/com/google/sitebricks/mail/test/integration/TestImap.java new file mode 100644 index 00000000..80a5d4f2 --- /dev/null +++ b/sitebricks-mail/src/test/java/com/google/sitebricks/mail/test/integration/TestImap.java @@ -0,0 +1,24 @@ +package com.google.sitebricks.mail.test.integration; + +import org.apache.james.imapserver.netty.IMAPServer; + +/** + * @author Mike Bain (mike@thealphatester.com) + */ +public class TestImap { + public static void main(String args[]) { + + IMAPServer imapServer = new IMAPServer(); + + + imapServer.setPort(7878); + + try { + imapServer.start(); + } catch (Exception e) { + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + + System.out.println("'Imap server started on port: " + imapServer.getPort() + "'"); + } +} diff --git a/sitebricks-mail/src/test/java/com/google/sitebricks/mail/webapp/Home.java b/sitebricks-mail/src/test/java/com/google/sitebricks/mail/webapp/Home.java new file mode 100644 index 00000000..6dcd2f3f --- /dev/null +++ b/sitebricks-mail/src/test/java/com/google/sitebricks/mail/webapp/Home.java @@ -0,0 +1,13 @@ +package com.google.sitebricks.mail.webapp; + +import com.google.sitebricks.At; +import com.google.sitebricks.Visible; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@At("/") +public class Home { + @Visible + public String message = "hi"; +} diff --git a/sitebricks-mail/src/test/java/com/google/sitebricks/mail/webapp/WebConfig.java b/sitebricks-mail/src/test/java/com/google/sitebricks/mail/webapp/WebConfig.java new file mode 100644 index 00000000..740eb36b --- /dev/null +++ b/sitebricks-mail/src/test/java/com/google/sitebricks/mail/webapp/WebConfig.java @@ -0,0 +1,21 @@ +package com.google.sitebricks.mail.webapp; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceServletContextListener; +import com.google.sitebricks.SitebricksModule; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class WebConfig extends GuiceServletContextListener { + @Override + protected Injector getInjector() { + return Guice.createInjector(new SitebricksModule() { + @Override + protected void configureSitebricks() { + scan(WebConfig.class.getPackage()); + } + }); + } +} diff --git a/sitebricks-mail/src/test/resources/Home.html b/sitebricks-mail/src/test/resources/Home.html new file mode 100644 index 00000000..c7e5c88d --- /dev/null +++ b/sitebricks-mail/src/test/resources/Home.html @@ -0,0 +1,10 @@ + + + + + + + Message is ${message} + + \ No newline at end of file diff --git a/sitebricks-mail/src/test/resources/WEB-INF/web.xml b/sitebricks-mail/src/test/resources/WEB-INF/web.xml new file mode 100644 index 00000000..caca00e8 --- /dev/null +++ b/sitebricks-mail/src/test/resources/WEB-INF/web.xml @@ -0,0 +1,22 @@ + + + + + + webFilter + com.google.inject.servlet.GuiceFilter + + + + webFilter + /* + + + + com.google.sitebricks.mail.webapp.WebConfig + + + diff --git a/sitebricks-mail/src/test/resources/com/google/sitebricks/mail/imap/fetch_all_data.txt b/sitebricks-mail/src/test/resources/com/google/sitebricks/mail/imap/fetch_all_data.txt new file mode 100644 index 00000000..99eff72c --- /dev/null +++ b/sitebricks-mail/src/test/resources/com/google/sitebricks/mail/imap/fetch_all_data.txt @@ -0,0 +1,9 @@ +1 FETCH (ENVELOPE ("Fri, 8 Apr 2011 23:12:09 -0700" "Get Gmail on your mobile phone" (("Gmail Team" NIL "mail-noreply" "google.com")) (("Gmail Team" NIL "mail-noreply" "google.com")) (("Gmail Team" NIL "mail-noreply" "google.com")) (("imap test" NIL "telnet.imap" "gmail.com")) NIL NIL NIL "") FLAGS () INTERNALDATE "09-Apr-2011 06:12:09 +0000" RFC822.SIZE 2439) +2 FETCH (ENVELOPE ("Fri, 8 Apr 2011 23:12:09 -0700" "Import your contacts and old email" (("Gmail Team" NIL "mail-noreply" "google.com")) (("Gmail Team" NIL "mail-noreply" "google.com")) (("Gmail Team" NIL "mail-noreply" "google.com")) (("imap test" NIL "telnet.imap" "gmail.com")) NIL NIL NIL "") FLAGS () INTERNALDATE "09-Apr-2011 06:12:09 +0000" RFC822.SIZE 2918) +3 FETCH (ENVELOPE ("Fri, 8 Apr 2011 23:12:09 -0700" "Customize Gmail with colors and themes" (("Gmail Team" NIL "mail-noreply" "google.com")) (("Gmail Team" NIL "mail-noreply" "google.com")) (("Gmail Team" NIL "mail-noreply" "google.com")) (("imap test" NIL "telnet.imap" "gmail.com")) NIL NIL NIL "") FLAGS () INTERNALDATE "09-Apr-2011 06:12:09 +0000" RFC822.SIZE 2747) +4 FETCH (ENVELOPE ("Sun, 10 Apr 2011 16:37:27 +1000" "test" (("telnet.imap" NIL "telnet.imap" "gmail.com")) (("telnet.imap" NIL "telnet.imap" "gmail.com")) (("telnet.imap" NIL "telnet.imap" "gmail.com")) ((NIL NIL "telnet_imap" "gmail.com")) NIL NIL NIL "<4DA15027.1070301@gmail.com>") FLAGS (\Seen) INTERNALDATE "10-Apr-2011 06:37:30 +0000" RFC822.SIZE 695) +5 FETCH (ENVELOPE ("Sun, 10 Apr 2011 06:37:31 +0000" "Delivery Status Notification (Failure)" (( "Mail Delivery Subsystem" NIL "mailer-daemon" "googlemail.com")) (("Mail Delivery Subsystem" NIL "mailer-daemon" "googlemail.com")) (("Mail Delivery Subsystem" NIL "mailer-daemon" "googlemail.com")) ((NIL NIL "telnet.imap" "gmail.com")) NIL NIL "<4DA15027.1070301@gmail.com>" "<000e0cd327a88645e404a08ab133@google.com>") FLAGS (\Seen) INTERNALDATE "10-Apr-2011 06:37:31 +0000" RFC822.SIZE 2346) +6 FETCH (ENVELOPE ("Sun, 10 Apr 2011 16:37:56 +1000" "test" (("telnet.imap" NIL "telnet.imap" "gmail.com")) (("telnet.imap" NIL "telnet.imap" "gmail.com")) (("telnet.imap" NIL "telnet.imap" "gmail.com")) ((NIL NIL "test.imap" "gmail.com")) NIL NIL NIL "<4DA15044.20500@gmail.com>") FLAGS (\Seen) INTERNALDATE "10-Apr-2011 06:37:59 +0000" RFC822.SIZE 697) +7 FETCH (ENVELOPE ("Sun, 10 Apr 2011 16:38:38 +1000" "test" (("Scott Bakula" NIL "test.account" " gmail. om")) (("Scott Bakula" NIL "agent.bain" "gmail.com")) (("Scott Bakula" NIL "test.account" "gmail.com")) ((NIL NIL "telnet.imap" "gmail.com")) NIL NIL NIL "") FLAGS () INTERNALDATE "10-Apr-2011 06:38:59 +0000" RFC822.SIZE 2005) +8 FETCH (ENVELOPE ("Tue, 26 Apr 2011 23:02:08 +1000" "Fwd: test push" (("Dhanji R. Prasanna" NIL "dhanji" "gmail.com")) (("Dhanji R. Prasanna" NIL "dhanji" "gmail.com")) (("Dhanji R. Prasanna" NIL "dhanji" "gmail.com")) ((NIL NIL "telnet.imap" "gmail.com")) NIL NIL "" "") FLAGS () INTERNALDATE "26-Apr-2011 13:02:08 +0000" RFC822.SIZE 2919) +9 FETCH (ENVELOPE ("Tue, 26 Apr 2011 23:03:34 +1000" "push" (("Dhanji R. Prasanna" NIL "dhanji" "gmail.com")) (("Dhanji R. Prasanna" NIL "dhanji" "gmail.com")) (("Dhanji R. Prasanna" NIL "dhanji" "gmail.com")) ((NIL NIL "telnet.imap" "gmail.com")) NIL NIL NIL "") FLAGS () INTERNALDATE "26-Apr-2011 13:03:34 +0000" RFC822.SIZE 2182) diff --git a/sitebricks-options/pom.xml b/sitebricks-options/pom.xml new file mode 100644 index 00000000..c3c552d8 --- /dev/null +++ b/sitebricks-options/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + com.google.sitebricks + sitebricks-parent + 0.8.5 + + sitebricks-options + Sitebricks :: Options Module + + + + jboss-maven2-public-repository + http://repository.jboss.org/nexus/content/groups/public-jboss/ + + false + + + + + + + com.google.sitebricks + sitebricks-converter + + + com.google.inject + guice + 3.0 + + + cglib + cglib-full + 2.0.2 + provided + + + org.testng + testng + 5.8 + jdk15 + test + + + + + sitebricks-options + + + src/test/resources + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + diff --git a/sitebricks-options/src/main/java/com/google/sitebricks/options/Options.java b/sitebricks-options/src/main/java/com/google/sitebricks/options/Options.java new file mode 100644 index 00000000..b63e15fd --- /dev/null +++ b/sitebricks-options/src/main/java/com/google/sitebricks/options/Options.java @@ -0,0 +1,18 @@ +package com.google.sitebricks.options; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation marker that identifies a type as a repository + * of options (example, command line options). + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Options { + String value() default ""; +} diff --git a/sitebricks-options/src/main/java/com/google/sitebricks/options/OptionsModule.java b/sitebricks-options/src/main/java/com/google/sitebricks/options/OptionsModule.java new file mode 100644 index 00000000..488bc0ee --- /dev/null +++ b/sitebricks-options/src/main/java/com/google/sitebricks/options/OptionsModule.java @@ -0,0 +1,192 @@ +package com.google.sitebricks.options; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.sitebricks.conversion.MvelTypeConverter; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.*; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class OptionsModule extends AbstractModule { + private final Map options; + + private final List> optionClasses = new ArrayList>(); + + public OptionsModule(String[] commandLine, Iterable> freeOptions) { + options = new HashMap(commandLine.length); + for (String option : commandLine) { + if (option.startsWith("--") && option.length() > 2) { + option = option.substring(2); + + String[] pair = option.split("=", 2); + if (pair.length == 1) { + options.put(pair[0], Boolean.TRUE.toString()); + } else { + options.put(pair[0], pair[1]); + } + } + } + + for (Map freeOptionMap : freeOptions) { + options.putAll(freeOptionMap); + } + } + + public OptionsModule(String[] commandLine) { + this(commandLine, ImmutableList.>of()); + } + + public OptionsModule(Iterable> freeOptions) { + this(new String[0], freeOptions); + } + + public OptionsModule(Properties... freeOptions) { + this(new String[0], toMaps(freeOptions)); + } + + public OptionsModule(ResourceBundle... freeOptions) { + this(new String[0], toMaps(freeOptions)); + } + + private static Iterable> toMaps(ResourceBundle[] freeOptions) { + List> maps = Lists.newArrayList(); + for (ResourceBundle bundle : freeOptions) { + Map asMap = Maps.newHashMap(); + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + asMap.put(key, bundle.getString(key)); + } + + maps.add(asMap); + } + return maps; + } + + private static Iterable> toMaps(Properties[] freeOptions) { + List> maps = Lists.newArrayList(); + for (Properties freeOption : freeOptions) { + maps.add(Maps.fromProperties(freeOption)); + } + return maps; + } + + @Override + protected final void configure() { + // Analyze options classes. + for (Class optionClass : optionClasses) { + + // If using abstract classes, detect cglib. + if (Modifier.isAbstract(optionClass.getModifiers())) { + try { + Class.forName("net.sf.cglib.proxy.Enhancer"); + } catch (ClassNotFoundException e) { + addError("Cannot use abstract @Option classes unless Cglib is on the classpath, " + + "[%s] was abstract. Hint: add Cglib 2.0.2 or better to classpath", + optionClass.getName()); + } + } + + String namespace = optionClass.getAnnotation(Options.class).value(); + if (!namespace.isEmpty()) + namespace += "."; + + // Construct a map that will contain the values needed to back the interface. + final Map concreteOptions = + new HashMap(optionClass.getDeclaredMethods().length); + boolean skipClass = false; + for (Method method : optionClass.getDeclaredMethods()) { + String key = namespace + method.getName(); + + String value = options.get(key); + + // Gather all the errors regarding @Options methods that have no specified config. + if (null == value && Modifier.isAbstract(method.getModifiers())) { + addError("Option '%s' specified in type [%s] is unavailable in provided configuration", + key, + optionClass); + skipClass = true; + break; + } + + // TODO Can we validate that the value is coercible into the return type correctly? + concreteOptions.put(method.getName(), value); + } + + if (!skipClass) { + Object instance; + if (optionClass.isInterface()) { + instance = createJdkProxyHandler(optionClass, concreteOptions); + } else { + instance = createCglibHandler(optionClass, concreteOptions); + } + + bind((Class) optionClass).toInstance(instance); + } + } + } + + private Object createJdkProxyHandler(Class optionClass, + final Map concreteOptions) { + final InvocationHandler handler = new InvocationHandler() { + @Inject + MvelTypeConverter converter; + + @Override + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + return converter.convert(concreteOptions.get(method.getName()), method.getReturnType()); + } + }; + requestInjection(handler); + return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), + new Class[]{optionClass}, handler); + } + + private Object createCglibHandler(Class optionClass, + final Map concreteOptions) { + MethodInterceptor interceptor = new MethodInterceptor() { + @Inject + MvelTypeConverter converter; + + @Override + public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) + throws Throwable { + String value = concreteOptions.get(method.getName()); + if (null == value) { + // Return the default value by calling the original method. + return methodProxy.invokeSuper(o, objects); + } + return converter.convert(value, method.getReturnType()); + } + }; + requestInjection(interceptor); + return Enhancer.create(optionClass, interceptor); + } + + public OptionsModule options(Class clazz) { + if (!clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers())) { + throw new IllegalArgumentException(String.format("%s must be an interface or abstract class", + clazz.getName())); + } + + if (!clazz.isAnnotationPresent(Options.class)) { + throw new IllegalArgumentException(String.format("%s must be annotated with @Options", + clazz.getName())); + } + + optionClasses.add(clazz); + return this; + } +} diff --git a/sitebricks-options/src/test/java/com/google/sitebricks/options/OptionsTest.java b/sitebricks-options/src/test/java/com/google/sitebricks/options/OptionsTest.java new file mode 100644 index 00000000..ee80e47d --- /dev/null +++ b/sitebricks-options/src/test/java/com/google/sitebricks/options/OptionsTest.java @@ -0,0 +1,141 @@ +package com.google.sitebricks.options; + +import com.google.inject.Guice; +import org.testng.annotations.Test; + +import java.util.Properties; +import java.util.ResourceBundle; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class OptionsTest { + + @Test + public final void testOptionsInterfaceFromCommandLine() { + String[] commandLine = + "--host=optimusprime --domain=http://sitebricks.info --otherSetting=twoForOne!1" + .split("[ ]+"); + + MyOpts opts = Guice.createInjector(new OptionsModule(commandLine).options(MyOpts.class)) + .getInstance(MyOpts.class); + + assert "optimusprime".equals(opts.host()); + assert "http://sitebricks.info".equals(opts.domain()); + assert "twoForOne!1".equals(opts.otherSetting()); + } + + @Test + public final void testOptionsInterfaceWithNamespaceFromCommandLine() { + + String[] commandLine = + ("--sitebricks.host=optimusprime --sitebricks.domain=http://sitebricks.info" + + " --sitebricks.otherSetting=twoForOne!1").split("[ ]+"); + + MyOpts2 opts = Guice.createInjector(new OptionsModule(commandLine).options(MyOpts2.class)) + .getInstance(MyOpts2.class); + + assert "optimusprime".equals(opts.host()); + assert "http://sitebricks.info".equals(opts.domain()); + assert "twoForOne!1".equals(opts.otherSetting()); + } + + @Test + public final void testTypedOptionsInterfaceFromCommandLine() { + + String[] commandLine = + ("--name=optimusprimer --score=0.8 --port=1034").split("[ ]+"); + + MyTypedOpts opts = Guice.createInjector(new OptionsModule(commandLine).options(MyTypedOpts.class)) + .getInstance(MyTypedOpts.class); + + assert "optimusprimer".equals(opts.name()); + assert new Double(0.8).equals(opts.score()); + assert 1034 == opts.port(); + } + + @Test + public final void testTypedOptionsInterfaceWithNamespaceFromProperties() { + Properties properties = new Properties(); + properties.put("name", "optimusprimer"); + properties.put("score", "0.7"); + properties.put("port", "65535"); + + MyTypedOpts opts = Guice.createInjector(new OptionsModule(properties) + .options(MyTypedOpts.class)) + .getInstance(MyTypedOpts.class); + + assert "optimusprimer".equals(opts.name()); + assert new Double(0.7).equals(opts.score()); + assert 65535 == opts.port(); + } + + @Test + public final void testTypedOptionsInterfaceWithNamespaceFromPropertiesFile() { + ResourceBundle bundle = ResourceBundle.getBundle(Options.class.getPackage().getName() + + ".options"); + MyTypedOpts opts = Guice.createInjector(new OptionsModule(bundle) + .options(MyTypedOpts.class)) + .getInstance(MyTypedOpts.class); + + assert "optimusprimer".equals(opts.name()); + assert new Double(0.7).equals(opts.score()); + assert 65534 == opts.port(); + } + + @Test + public final void testTypedOptionsAbstractClassWithNamespaceFromPropertiesFile() { + ResourceBundle bundle = ResourceBundle.getBundle(Options.class.getPackage().getName() + + ".options"); + MyAbstractOpts opts = Guice.createInjector(new OptionsModule(bundle) + .options(MyAbstractOpts.class)) + .getInstance(MyAbstractOpts.class); + + assert "optimusprimer".equals(opts.name()); + assert new Double(0.7).equals(opts.score()); + assert 65534 == opts.port(); // Default overridden. + assert 22 == opts.code(); // Default. + } + + @Options + public static interface MyOpts { + String host(); + + String domain(); + + String otherSetting(); + } + + @Options("sitebricks") + public static interface MyOpts2 { + String host(); + + String domain(); + + String otherSetting(); + } + + @Options + public static interface MyTypedOpts { + String name(); + + Double score(); + + int port(); + } + + @Options + public static abstract class MyAbstractOpts { + abstract String name(); + + abstract Double score(); + + int port() { + return 22; + } + + int code() { + return 22; + } + } +} diff --git a/sitebricks-options/src/test/resources/com/google/sitebricks/options/options.properties b/sitebricks-options/src/test/resources/com/google/sitebricks/options/options.properties new file mode 100644 index 00000000..0468d1a3 --- /dev/null +++ b/sitebricks-options/src/test/resources/com/google/sitebricks/options/options.properties @@ -0,0 +1,3 @@ +name=optimusprimer +score=0.7 +port=65534 \ No newline at end of file diff --git a/sitebricks-web/pom.xml b/sitebricks-web/pom.xml new file mode 100644 index 00000000..0da273ba --- /dev/null +++ b/sitebricks-web/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + com.google.sitebricks + sitebricks-parent + 0.8.5-SNAPSHOT + + sitebricks-web + Sitebricks :: Info Website + + + scala-tools + http://scala-tools.org/repo-releases + + + http://source.db4o.com/maven + http://source.db4o.com/maven + + true + + + + + + + com.google.sitebricks + sitebricks + + + org.mortbay.jetty + jetty + + + org.mortbay.jetty + jetty-util + + + org.mortbay.jetty + servlet-api-2.5 + + + org.markdownj + markdownj + 0.3.0-1.0.2b4 + + + org.apache.lucene + lucene-core + 3.0.2 + + + com.db4o + db4o-full-java5 + 8.0-SNAPSHOT + + + ch.qos.logback + logback-classic + ${ch.qos.logback.version} + + + diff --git a/sitebricks-web/src/main/java/info/sitebricks/Jetty.java b/sitebricks-web/src/main/java/info/sitebricks/Jetty.java new file mode 100644 index 00000000..d3dd3caa --- /dev/null +++ b/sitebricks-web/src/main/java/info/sitebricks/Jetty.java @@ -0,0 +1,45 @@ +package info.sitebricks; + +import org.mortbay.jetty.Server; +import org.mortbay.jetty.webapp.WebAppContext; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class Jetty { + private static final String APP_NAME = ""; + private static final int PORT = 4040; + + private final Server server; + + public Jetty() { + this(new WebAppContext("src/main/resources", APP_NAME), PORT); + } + + public Jetty(String path) { + this(new WebAppContext(path, APP_NAME), PORT); + } + + public Jetty(WebAppContext webAppContext, int port) { + server = new Server(port); + server.addHandler(webAppContext); + } + + public void start() throws Exception { + server.start(); + } + + public void join() throws Exception { + server.join(); + } + + public void stop() throws Exception { + server.stop(); + } + + public static void main(String... args) throws Exception { + Jetty jetty = new Jetty(); + jetty.start(); + jetty.join(); + } +} diff --git a/sitebricks-web/src/main/java/info/sitebricks/SitebricksConfig.java b/sitebricks-web/src/main/java/info/sitebricks/SitebricksConfig.java new file mode 100644 index 00000000..dff0f1b8 --- /dev/null +++ b/sitebricks-web/src/main/java/info/sitebricks/SitebricksConfig.java @@ -0,0 +1,32 @@ +package info.sitebricks; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceServletContextListener; +import com.google.inject.servlet.ServletModule; +import com.google.sitebricks.SitebricksModule; +import info.sitebricks.persist.PersistFilter; +import info.sitebricks.persist.StoreModule; +import info.sitebricks.web.WikiService; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class SitebricksConfig extends GuiceServletContextListener { + @Override + protected Injector getInjector() { + return Guice.createInjector(new ServletModule() { + @Override + protected void configureServlets() { + install(new StoreModule()); + filter("/*").through(PersistFilter.class); + } + + }, new SitebricksModule() { + @Override + protected void configureSitebricks() { + scan(WikiService.class.getPackage()); + } + }); + } +} diff --git a/sitebricks-web/src/main/java/info/sitebricks/data/Document.java b/sitebricks-web/src/main/java/info/sitebricks/data/Document.java new file mode 100644 index 00000000..bc88f2bd --- /dev/null +++ b/sitebricks-web/src/main/java/info/sitebricks/data/Document.java @@ -0,0 +1,79 @@ +package info.sitebricks.data; + +import com.google.sitebricks.Show; +import com.petebevin.markdown.MarkdownProcessor; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * A single wiki page/document. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@Show("Document.html") +public class Document { + private static final DateFormat format = new SimpleDateFormat("MMM dd"); + + public static final String HOME = "Home"; + private String author; + private Date createdOn; + private String topic; + private String name; // a unique page-name, typically derived from the topic + private String text; // markdown format. + private MarkdownProcessor markdownProcessor; + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + + // set the name too. + this.name = topic.replaceAll("[|_!?&%$\"'#;:.,+\\\\(\\){}]+", "") + .replaceAll("[ ]+", "-") + .toLowerCase(); + } + + public String getName() { + return name; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String format(Date date) { + return format.format(date); + } + + public String markdown() { + return markdownProcessor.markdown(text); + } + + public void setTemporaryMarkdownProcessor(MarkdownProcessor temporaryMarkdownProcessor) { + this.markdownProcessor = temporaryMarkdownProcessor; + } +} diff --git a/sitebricks-web/src/main/java/info/sitebricks/data/Index.java b/sitebricks-web/src/main/java/info/sitebricks/data/Index.java new file mode 100644 index 00000000..a33df31e --- /dev/null +++ b/sitebricks-web/src/main/java/info/sitebricks/data/Index.java @@ -0,0 +1,53 @@ +package info.sitebricks.data; + +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * Hierarchical index of wiki documents. (Singleton) + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class Index { + private Node root; + + /** needed by db4o **/ + public Index() { + } + + public Index(Node root) { + this.root = root; + } + + /** + * Returns the list of top-level index items. + */ + public List list() { + return root.getChildren(); + } + + public static class Node { + private String name; // directly maps to Document#name + private String topic; + + private List nodes = Lists.newArrayList(); + + public String getTopic() { + return topic; + } + + public String getName() { + return name; + } + + public List getChildren() { + return nodes; + } + + public void setDocument(Document document) { + this.name = document.getName(); + this.topic = document.getTopic(); + } + } +} diff --git a/sitebricks-web/src/main/java/info/sitebricks/persist/PersistAware.java b/sitebricks-web/src/main/java/info/sitebricks/persist/PersistAware.java new file mode 100644 index 00000000..248e6fcc --- /dev/null +++ b/sitebricks-web/src/main/java/info/sitebricks/persist/PersistAware.java @@ -0,0 +1,31 @@ +package info.sitebricks.persist; + +import com.db4o.Db4o; +import com.db4o.ObjectContainer; +import com.db4o.ObjectServer; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.sitebricks.Aware; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@Singleton +class PersistAware implements Aware, Provider { + private ObjectServer objectServer; + + @Override @SuppressWarnings("deprecation") + public void startup() { + objectServer = Db4o.openServer("sitebricks-web.dat", 65535 /* port ignored */); + } + + @Override + public void shutdown() { + objectServer.close(); + } + + @Override + public ObjectContainer get() { + return objectServer.openClient(); + } +} diff --git a/sitebricks-web/src/main/java/info/sitebricks/persist/PersistFilter.java b/sitebricks-web/src/main/java/info/sitebricks/persist/PersistFilter.java new file mode 100644 index 00000000..53c6e597 --- /dev/null +++ b/sitebricks-web/src/main/java/info/sitebricks/persist/PersistFilter.java @@ -0,0 +1,46 @@ +package info.sitebricks.persist; + +import com.db4o.ObjectContainer; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@Singleton +public class PersistFilter implements Filter { + private final Provider container; + + @Inject + public PersistFilter(Provider container) { + this.container = container; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + try { + container.get(); + filterChain.doFilter(servletRequest, servletResponse); + } finally { + container.get().commit(); + } + } + + @Override + public void destroy() { + } +} diff --git a/sitebricks-web/src/main/java/info/sitebricks/persist/StoreModule.java b/sitebricks-web/src/main/java/info/sitebricks/persist/StoreModule.java new file mode 100644 index 00000000..3c4d641a --- /dev/null +++ b/sitebricks-web/src/main/java/info/sitebricks/persist/StoreModule.java @@ -0,0 +1,17 @@ +package info.sitebricks.persist; + +import com.db4o.ObjectContainer; +import com.google.inject.servlet.RequestScoped; +import com.google.sitebricks.AwareModule; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class StoreModule extends AwareModule { + @Override + protected void configureLifecycle() { + observe(PersistAware.class); + + bind(ObjectContainer.class).toProvider(PersistAware.class).in(RequestScoped.class); + } +} diff --git a/sitebricks-web/src/main/java/info/sitebricks/persist/WikiStore.java b/sitebricks-web/src/main/java/info/sitebricks/persist/WikiStore.java new file mode 100644 index 00000000..88f07676 --- /dev/null +++ b/sitebricks-web/src/main/java/info/sitebricks/persist/WikiStore.java @@ -0,0 +1,67 @@ +package info.sitebricks.persist; + +import com.db4o.ObjectContainer; +import com.db4o.ObjectSet; +import com.db4o.query.Predicate; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import info.sitebricks.data.Document; +import info.sitebricks.data.Index; +import info.sitebricks.data.Index.Node; + +import java.util.Date; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@Singleton +public class WikiStore { + private final Provider container; + + @Inject + public WikiStore(Provider container) { + this.container = container; + } + + public void store(Document document) { + container.get().store(document); + } + + // Names are unique. + public Document fetch(final String name) { + ObjectSet set = container.get().query(new Predicate() { + @Override + public boolean match(Document candidate) { + return name.equals(candidate.getName()); + } + }); + return set.isEmpty() ? null : set.next(); + } + + // TODO Cache index in memory? + public Index fetchIndex() { + ObjectContainer objects = container.get(); + ObjectSet set = objects.query(Index.class); + + // Create the index if none exists (first time ever)-- not threadsafe!! + if (set.isEmpty()) { + Document home = new Document(); + home.setTopic(Document.HOME); // implicitly sets the doc name + home.setAuthor("dhanji"); + home.setCreatedOn(new Date()); + home.setText(""); + objects.store(home); + + Node root = new Node(); + root.setDocument(home); + Index index = new Index(root); + index.list().add(root); + + objects.store(index); + return index; + } + + return set.next(); + } +} diff --git a/sitebricks-web/src/main/java/info/sitebricks/web/Home.java b/sitebricks-web/src/main/java/info/sitebricks/web/Home.java new file mode 100644 index 00000000..5a83266b --- /dev/null +++ b/sitebricks-web/src/main/java/info/sitebricks/web/Home.java @@ -0,0 +1,50 @@ +package info.sitebricks.web; + +import com.google.inject.Inject; +import com.google.sitebricks.At; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.rendering.Templates; +import com.petebevin.markdown.MarkdownProcessor; +import info.sitebricks.data.Document; +import info.sitebricks.data.Index; +import info.sitebricks.persist.WikiStore; + +import javax.servlet.http.HttpServletResponse; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@At("/") +public class Home { + private final WikiStore store; + private final MarkdownProcessor markdownProcessor; + private final Templates templates; + + @Inject + public Home(WikiStore store, MarkdownProcessor markdownProcessor, Templates templates) { + this.store = store; + this.markdownProcessor = markdownProcessor; + this.templates = templates; + } + + private Document home; + private Index index; + + @Get + public void home(HttpServletResponse response) { + response.setContentType(WikiService.TEXT_HTML_CHARSET_UTF8); + + index = store.fetchIndex(); // ensures "Home" exists + home = store.fetch("home"); + } + + public String renderHome() { + home.setTemporaryMarkdownProcessor(markdownProcessor); + + return templates.render(Document.class, home); + } + + public Index getIndex() { + return index; + } +} diff --git a/sitebricks-web/src/main/java/info/sitebricks/web/WikiService.java b/sitebricks-web/src/main/java/info/sitebricks/web/WikiService.java new file mode 100644 index 00000000..64d3cf92 --- /dev/null +++ b/sitebricks-web/src/main/java/info/sitebricks/web/WikiService.java @@ -0,0 +1,50 @@ +package info.sitebricks.web; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import com.google.sitebricks.At; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.headless.Service; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.rendering.Templates; +import com.petebevin.markdown.MarkdownProcessor; +import info.sitebricks.data.Document; +import info.sitebricks.persist.WikiStore; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@At("/ajax") @Service +public class WikiService { + public static final String TEXT_HTML_CHARSET_UTF8 = "text/html; charset=utf8"; + private final WikiStore wikiStore; + private final MarkdownProcessor markdownProcessor; + private final Templates templates; + + @Inject + public WikiService(WikiStore wikiStore, MarkdownProcessor markdownProcessor, + Templates templates) { + this.wikiStore = wikiStore; + this.markdownProcessor = markdownProcessor; + this.templates = templates; + } + + @At("/page/:name") @Post + Reply renderedPage(@Named("name") String name) { + // Look up page by name, then render it with markdownj. + Document document = wikiStore.fetch(name); + document.setTemporaryMarkdownProcessor(markdownProcessor); + + return Reply.with(templates.render(Document.class, document)) + .type(TEXT_HTML_CHARSET_UTF8); + } + + @At("/markdown/:name") @Post + Reply markdown(@Named("name") String name) { + // Look up page by name, then return the raw markdown. + Document document = wikiStore.fetch(name); + + return Reply.with(document.getText()) + .type(TEXT_HTML_CHARSET_UTF8); + } +} diff --git a/sitebricks-web/src/main/resources/Home.html b/sitebricks-web/src/main/resources/Home.html new file mode 100644 index 00000000..ba2d2a43 --- /dev/null +++ b/sitebricks-web/src/main/resources/Home.html @@ -0,0 +1,52 @@ + + + + Sitebricks :: A Web platform + + + + + + + +

+ +
+ +
+

sitebricks.info

+ +
+
+ +
+ ${this.renderHome()} +
+
+
+
+ + \ No newline at end of file diff --git a/sitebricks-web/src/main/resources/WEB-INF/web.xml b/sitebricks-web/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..45ea3b34 --- /dev/null +++ b/sitebricks-web/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,21 @@ + + + + + webFilter + com.google.inject.servlet.GuiceFilter + + + + webFilter + /* + + + + info.sitebricks.SitebricksConfig + + + diff --git a/sitebricks-web/src/main/resources/info/sitebricks/data/Document.html b/sitebricks-web/src/main/resources/info/sitebricks/data/Document.html new file mode 100644 index 00000000..d8043340 --- /dev/null +++ b/sitebricks-web/src/main/resources/info/sitebricks/data/Document.html @@ -0,0 +1,15 @@ + + + +
+
Updated + by + + ${this.author} + +
+

${this.topic}

+ ${this.markdown()} +
+ + \ No newline at end of file diff --git a/sitebricks-web/src/main/resources/js/jquery-1.4.3.min.js b/sitebricks-web/src/main/resources/js/jquery-1.4.3.min.js new file mode 100644 index 00000000..66abfd14 --- /dev/null +++ b/sitebricks-web/src/main/resources/js/jquery-1.4.3.min.js @@ -0,0 +1,166 @@ +/*! + * jQuery JavaScript Library v1.4.3 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu Oct 14 23:10:06 2010 -0400 + */ +(function(E,A){function U(){return false}function ba(){return true}function ja(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function Ga(a){var b,d,e=[],f=[],h,k,l,n,s,v,B,D;k=c.data(this,this.nodeType?"events":"__events__");if(typeof k==="function")k=k.events;if(!(a.liveFired===this||!k||!k.live||a.button&&a.type==="click")){if(a.namespace)D=RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)");a.liveFired=this;var H=k.live.slice(0);for(n=0;nd)break;a.currentTarget=f.elem;a.data=f.handleObj.data; +a.handleObj=f.handleObj;D=f.handleObj.origHandler.apply(f.elem,arguments);if(D===false||a.isPropagationStopped()){d=f.level;if(D===false)b=false}}return b}}function Y(a,b){return(a&&a!=="*"?a+".":"")+b.replace(Ha,"`").replace(Ia,"&")}function ka(a,b,d){if(c.isFunction(b))return c.grep(a,function(f,h){return!!b.call(f,h,f)===d});else if(b.nodeType)return c.grep(a,function(f){return f===b===d});else if(typeof b==="string"){var e=c.grep(a,function(f){return f.nodeType===1});if(Ja.test(b))return c.filter(b, +e,!d);else b=c.filter(b,e)}return c.grep(a,function(f){return c.inArray(f,b)>=0===d})}function la(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var e=c.data(a[d++]),f=c.data(this,e);if(e=e&&e.events){delete f.handle;f.events={};for(var h in e)for(var k in e[h])c.event.add(this,h,e[h][k],e[h][k].data)}}})}function Ka(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)} +function ma(a,b,d){var e=b==="width"?a.offsetWidth:a.offsetHeight;if(d==="border")return e;c.each(b==="width"?La:Ma,function(){d||(e-=parseFloat(c.css(a,"padding"+this))||0);if(d==="margin")e+=parseFloat(c.css(a,"margin"+this))||0;else e-=parseFloat(c.css(a,"border"+this+"Width"))||0});return e}function ca(a,b,d,e){if(c.isArray(b)&&b.length)c.each(b,function(f,h){d||Na.test(a)?e(a,h):ca(a+"["+(typeof h==="object"||c.isArray(h)?f:"")+"]",h,d,e)});else if(!d&&b!=null&&typeof b==="object")c.isEmptyObject(b)? +e(a,""):c.each(b,function(f,h){ca(a+"["+f+"]",h,d,e)});else e(a,b)}function S(a,b){var d={};c.each(na.concat.apply([],na.slice(0,b)),function(){d[this]=a});return d}function oa(a){if(!da[a]){var b=c("<"+a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d==="")d="block";da[a]=d}return da[a]}function ea(a){return c.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var u=E.document,c=function(){function a(){if(!b.isReady){try{u.documentElement.doScroll("left")}catch(i){setTimeout(a, +1);return}b.ready()}}var b=function(i,r){return new b.fn.init(i,r)},d=E.jQuery,e=E.$,f,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,k=/\S/,l=/^\s+/,n=/\s+$/,s=/\W/,v=/\d/,B=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,D=/^[\],:{}\s]*$/,H=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,w=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,G=/(?:^|:|,)(?:\s*\[)+/g,M=/(webkit)[ \/]([\w.]+)/,g=/(opera)(?:.*version)?[ \/]([\w.]+)/,j=/(msie) ([\w.]+)/,o=/(mozilla)(?:.*? rv:([\w.]+))?/,m=navigator.userAgent,p=false, +q=[],t,x=Object.prototype.toString,C=Object.prototype.hasOwnProperty,P=Array.prototype.push,N=Array.prototype.slice,R=String.prototype.trim,Q=Array.prototype.indexOf,L={};b.fn=b.prototype={init:function(i,r){var y,z,F;if(!i)return this;if(i.nodeType){this.context=this[0]=i;this.length=1;return this}if(i==="body"&&!r&&u.body){this.context=u;this[0]=u.body;this.selector="body";this.length=1;return this}if(typeof i==="string")if((y=h.exec(i))&&(y[1]||!r))if(y[1]){F=r?r.ownerDocument||r:u;if(z=B.exec(i))if(b.isPlainObject(r)){i= +[u.createElement(z[1])];b.fn.attr.call(i,r,true)}else i=[F.createElement(z[1])];else{z=b.buildFragment([y[1]],[F]);i=(z.cacheable?z.fragment.cloneNode(true):z.fragment).childNodes}return b.merge(this,i)}else{if((z=u.getElementById(y[2]))&&z.parentNode){if(z.id!==y[2])return f.find(i);this.length=1;this[0]=z}this.context=u;this.selector=i;return this}else if(!r&&!s.test(i)){this.selector=i;this.context=u;i=u.getElementsByTagName(i);return b.merge(this,i)}else return!r||r.jquery?(r||f).find(i):b(r).find(i); +else if(b.isFunction(i))return f.ready(i);if(i.selector!==A){this.selector=i.selector;this.context=i.context}return b.makeArray(i,this)},selector:"",jquery:"1.4.3",length:0,size:function(){return this.length},toArray:function(){return N.call(this,0)},get:function(i){return i==null?this.toArray():i<0?this.slice(i)[0]:this[i]},pushStack:function(i,r,y){var z=b();b.isArray(i)?P.apply(z,i):b.merge(z,i);z.prevObject=this;z.context=this.context;if(r==="find")z.selector=this.selector+(this.selector?" ": +"")+y;else if(r)z.selector=this.selector+"."+r+"("+y+")";return z},each:function(i,r){return b.each(this,i,r)},ready:function(i){b.bindReady();if(b.isReady)i.call(u,b);else q&&q.push(i);return this},eq:function(i){return i===-1?this.slice(i):this.slice(i,+i+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(N.apply(this,arguments),"slice",N.call(arguments).join(","))},map:function(i){return this.pushStack(b.map(this,function(r,y){return i.call(r, +y,r)}))},end:function(){return this.prevObject||b(null)},push:P,sort:[].sort,splice:[].splice};b.fn.init.prototype=b.fn;b.extend=b.fn.extend=function(){var i=arguments[0]||{},r=1,y=arguments.length,z=false,F,I,K,J,fa;if(typeof i==="boolean"){z=i;i=arguments[1]||{};r=2}if(typeof i!=="object"&&!b.isFunction(i))i={};if(y===r){i=this;--r}for(;r0)){if(q){for(var r=0;i=q[r++];)i.call(u,b);q=null}b.fn.triggerHandler&&b(u).triggerHandler("ready")}}},bindReady:function(){if(!p){p=true;if(u.readyState==="complete")return setTimeout(b.ready, +1);if(u.addEventListener){u.addEventListener("DOMContentLoaded",t,false);E.addEventListener("load",b.ready,false)}else if(u.attachEvent){u.attachEvent("onreadystatechange",t);E.attachEvent("onload",b.ready);var i=false;try{i=E.frameElement==null}catch(r){}u.documentElement.doScroll&&i&&a()}}},isFunction:function(i){return b.type(i)==="function"},isArray:Array.isArray||function(i){return b.type(i)==="array"},isWindow:function(i){return i&&typeof i==="object"&&"setInterval"in i},isNaN:function(i){return i== +null||!v.test(i)||isNaN(i)},type:function(i){return i==null?String(i):L[x.call(i)]||"object"},isPlainObject:function(i){if(!i||b.type(i)!=="object"||i.nodeType||b.isWindow(i))return false;if(i.constructor&&!C.call(i,"constructor")&&!C.call(i.constructor.prototype,"isPrototypeOf"))return false;for(var r in i);return r===A||C.call(i,r)},isEmptyObject:function(i){for(var r in i)return false;return true},error:function(i){throw i;},parseJSON:function(i){if(typeof i!=="string"||!i)return null;i=b.trim(i); +if(D.test(i.replace(H,"@").replace(w,"]").replace(G,"")))return E.JSON&&E.JSON.parse?E.JSON.parse(i):(new Function("return "+i))();else b.error("Invalid JSON: "+i)},noop:function(){},globalEval:function(i){if(i&&k.test(i)){var r=u.getElementsByTagName("head")[0]||u.documentElement,y=u.createElement("script");y.type="text/javascript";if(b.support.scriptEval)y.appendChild(u.createTextNode(i));else y.text=i;r.insertBefore(y,r.firstChild);r.removeChild(y)}},nodeName:function(i,r){return i.nodeName&&i.nodeName.toUpperCase()=== +r.toUpperCase()},each:function(i,r,y){var z,F=0,I=i.length,K=I===A||b.isFunction(i);if(y)if(K)for(z in i){if(r.apply(i[z],y)===false)break}else for(;F";a=u.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var s=u.createElement("div"); +s.style.width=s.style.paddingLeft="1px";u.body.appendChild(s);c.boxModel=c.support.boxModel=s.offsetWidth===2;if("zoom"in s.style){s.style.display="inline";s.style.zoom=1;c.support.inlineBlockNeedsLayout=s.offsetWidth===2;s.style.display="";s.innerHTML="
";c.support.shrinkWrapBlocks=s.offsetWidth!==2}s.innerHTML="
t
";var v=s.getElementsByTagName("td");c.support.reliableHiddenOffsets=v[0].offsetHeight=== +0;v[0].style.display="";v[1].style.display="none";c.support.reliableHiddenOffsets=c.support.reliableHiddenOffsets&&v[0].offsetHeight===0;s.innerHTML="";u.body.removeChild(s).style.display="none"});a=function(s){var v=u.createElement("div");s="on"+s;var B=s in v;if(!B){v.setAttribute(s,"return;");B=typeof v[s]==="function"}return B};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=f=h=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength", +cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var pa={},Oa=/^(?:\{.*\}|\[.*\])$/;c.extend({cache:{},uuid:0,expando:"jQuery"+c.now(),noData:{embed:true,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:true},data:function(a,b,d){if(c.acceptData(a)){a=a==E?pa:a;var e=a.nodeType,f=e?a[c.expando]:null,h=c.cache;if(!(e&&!f&&typeof b==="string"&&d===A)){if(e)f||(a[c.expando]=f=++c.uuid);else h=a;if(typeof b==="object")if(e)h[f]= +c.extend(h[f],b);else c.extend(h,b);else if(e&&!h[f])h[f]={};a=e?h[f]:h;if(d!==A)a[b]=d;return typeof b==="string"?a[b]:a}}},removeData:function(a,b){if(c.acceptData(a)){a=a==E?pa:a;var d=a.nodeType,e=d?a[c.expando]:a,f=c.cache,h=d?f[e]:e;if(b){if(h){delete h[b];d&&c.isEmptyObject(h)&&c.removeData(a)}}else if(d&&c.support.deleteExpando)delete a[c.expando];else if(a.removeAttribute)a.removeAttribute(c.expando);else if(d)delete f[e];else for(var k in a)delete a[k]}},acceptData:function(a){if(a.nodeName){var b= +c.noData[a.nodeName.toLowerCase()];if(b)return!(b===true||a.getAttribute("classid")!==b)}return true}});c.fn.extend({data:function(a,b){if(typeof a==="undefined")return this.length?c.data(this[0]):null;else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===A){var e=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(e===A&&this.length){e=c.data(this[0],a);if(e===A&&this[0].nodeType===1){e=this[0].getAttribute("data-"+a);if(typeof e=== +"string")try{e=e==="true"?true:e==="false"?false:e==="null"?null:!c.isNaN(e)?parseFloat(e):Oa.test(e)?c.parseJSON(e):e}catch(f){}else e=A}}return e===A&&d[1]?this.data(d[0]):e}else return this.each(function(){var h=c(this),k=[d[0],b];h.triggerHandler("setData"+d[1]+"!",k);c.data(this,a,b);h.triggerHandler("changeData"+d[1]+"!",k)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var e=c.data(a,b);if(!d)return e|| +[];if(!e||c.isArray(d))e=c.data(a,b,c.makeArray(d));else e.push(d);return e}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),e=d.shift();if(e==="inprogress")e=d.shift();if(e){b==="fx"&&d.unshift("inprogress");e.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===A)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this, +a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var qa=/[\n\t]/g,ga=/\s+/,Pa=/\r/g,Qa=/^(?:href|src|style)$/,Ra=/^(?:button|input)$/i,Sa=/^(?:button|input|object|select|textarea)$/i,Ta=/^a(?:rea)?$/i,ra=/^(?:radio|checkbox)$/i;c.fn.extend({attr:function(a,b){return c.access(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this, +a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(s){var v=c(this);v.addClass(a.call(this,s,v.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ga),d=0,e=this.length;d-1)return true;return false}, +val:function(a){if(!arguments.length){var b=this[0];if(b){if(c.nodeName(b,"option")){var d=b.attributes.value;return!d||d.specified?b.value:b.text}if(c.nodeName(b,"select")){var e=b.selectedIndex;d=[];var f=b.options;b=b.type==="select-one";if(e<0)return null;var h=b?e:0;for(e=b?e+1:f.length;h=0;else if(c.nodeName(this,"select")){var B=c.makeArray(v);c("option",this).each(function(){this.selected= +c.inArray(c(this).val(),B)>=0});if(!B.length)this.selectedIndex=-1}else this.value=v}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,e){if(!a||a.nodeType===3||a.nodeType===8)return A;if(e&&b in c.attrFn)return c(a)[b](d);e=a.nodeType!==1||!c.isXMLDoc(a);var f=d!==A;b=e&&c.props[b]||b;if(a.nodeType===1){var h=Qa.test(b);if((b in a||a[b]!==A)&&e&&!h){if(f){b==="type"&&Ra.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); +if(d===null)a.nodeType===1&&a.removeAttribute(b);else a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:Sa.test(a.nodeName)||Ta.test(a.nodeName)&&a.href?0:A;return a[b]}if(!c.support.style&&e&&b==="style"){if(f)a.style.cssText=""+d;return a.style.cssText}f&&a.setAttribute(b,""+d);if(!a.attributes[b]&&a.hasAttribute&&!a.hasAttribute(b))return A;a=!c.support.hrefNormalized&&e&& +h?a.getAttribute(b,2):a.getAttribute(b);return a===null?A:a}}});var X=/\.(.*)$/,ha=/^(?:textarea|input|select)$/i,Ha=/\./g,Ia=/ /g,Ua=/[^\w\s.|`]/g,Va=function(a){return a.replace(Ua,"\\$&")},sa={focusin:0,focusout:0};c.event={add:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(c.isWindow(a)&&a!==E&&!a.frameElement)a=E;if(d===false)d=U;var f,h;if(d.handler){f=d;d=f.handler}if(!d.guid)d.guid=c.guid++;if(h=c.data(a)){var k=a.nodeType?"events":"__events__",l=h[k],n=h.handle;if(typeof l=== +"function"){n=l.handle;l=l.events}else if(!l){a.nodeType||(h[k]=h=function(){});h.events=l={}}if(!n)h.handle=n=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(n.elem,arguments):A};n.elem=a;b=b.split(" ");for(var s=0,v;k=b[s++];){h=f?c.extend({},f):{handler:d,data:e};if(k.indexOf(".")>-1){v=k.split(".");k=v.shift();h.namespace=v.slice(0).sort().join(".")}else{v=[];h.namespace=""}h.type=k;if(!h.guid)h.guid=d.guid;var B=l[k],D=c.event.special[k]||{};if(!B){B=l[k]=[]; +if(!D.setup||D.setup.call(a,e,v,n)===false)if(a.addEventListener)a.addEventListener(k,n,false);else a.attachEvent&&a.attachEvent("on"+k,n)}if(D.add){D.add.call(a,h);if(!h.handler.guid)h.handler.guid=d.guid}B.push(h);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(d===false)d=U;var f,h,k=0,l,n,s,v,B,D,H=a.nodeType?"events":"__events__",w=c.data(a),G=w&&w[H];if(w&&G){if(typeof G==="function"){w=G;G=G.events}if(b&&b.type){d=b.handler;b=b.type}if(!b|| +typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(f in G)c.event.remove(a,f+b)}else{for(b=b.split(" ");f=b[k++];){v=f;l=f.indexOf(".")<0;n=[];if(!l){n=f.split(".");f=n.shift();s=RegExp("(^|\\.)"+c.map(n.slice(0).sort(),Va).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(B=G[f])if(d){v=c.event.special[f]||{};for(h=e||0;h=0){a.type= +f=f.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[f]&&c.each(c.cache,function(){this.events&&this.events[f]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return A;a.result=A;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(e=d.nodeType?c.data(d,"handle"):(c.data(d,"__events__")||{}).handle)&&e.apply(d,b);e=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+f]&&d["on"+f].apply(d,b)=== +false){a.result=false;a.preventDefault()}}catch(h){}if(!a.isPropagationStopped()&&e)c.event.trigger(a,b,e,true);else if(!a.isDefaultPrevented()){e=a.target;var k,l=f.replace(X,""),n=c.nodeName(e,"a")&&l==="click",s=c.event.special[l]||{};if((!s._default||s._default.call(d,a)===false)&&!n&&!(e&&e.nodeName&&c.noData[e.nodeName.toLowerCase()])){try{if(e[l]){if(k=e["on"+l])e["on"+l]=null;c.event.triggered=true;e[l]()}}catch(v){}if(k)e["on"+l]=k;c.event.triggered=false}}},handle:function(a){var b,d,e; +d=[];var f,h=c.makeArray(arguments);a=h[0]=c.event.fix(a||E.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;if(!b){e=a.type.split(".");a.type=e.shift();d=e.slice(0).sort();e=RegExp("(^|\\.)"+d.join("\\.(?:.*\\.)?")+"(\\.|$)")}a.namespace=a.namespace||d.join(".");f=c.data(this,this.nodeType?"events":"__events__");if(typeof f==="function")f=f.events;d=(f||{})[a.type];if(f&&d){d=d.slice(0);f=0;for(var k=d.length;f-1?c.map(a.options,function(e){return e.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},Z=function(a,b){var d=a.target,e,f;if(!(!ha.test(d.nodeName)||d.readOnly)){e=c.data(d,"_change_data");f=va(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",f);if(!(e===A||f===e))if(e!=null||f){a.type="change";a.liveFired= +A;return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:Z,beforedeactivate:Z,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return Z.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return Z.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,"_change_data",va(a))}},setup:function(){if(this.type=== +"file")return false;for(var a in V)c.event.add(this,a+".specialChange",V[a]);return ha.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return ha.test(this.nodeName)}};V=c.event.special.change.filters;V.focus=V.beforeactivate}u.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(e){e=c.event.fix(e);e.type=b;return c.event.trigger(e,null,e.target)}c.event.special[b]={setup:function(){sa[b]++===0&&u.addEventListener(a,d,true)},teardown:function(){--sa[b]=== +0&&u.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,e,f){if(typeof d==="object"){for(var h in d)this[b](h,e,d[h],f);return this}if(c.isFunction(e)||e===false){f=e;e=A}var k=b==="one"?c.proxy(f,function(n){c(this).unbind(n,k);return f.apply(this,arguments)}):f;if(d==="unload"&&b!=="one")this.one(d,e,f);else{h=0;for(var l=this.length;h0?this.bind(b,d,e):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});E.attachEvent&&!E.addEventListener&&c(E).bind("unload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}}); +(function(){function a(g,j,o,m,p,q){p=0;for(var t=m.length;p0){C=x;break}}x=x[g]}m[p]=C}}}var d=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,h=false,k=true;[0,0].sort(function(){k=false;return 0});var l=function(g,j,o,m){o=o||[];var p=j=j||u;if(j.nodeType!==1&&j.nodeType!==9)return[];if(!g||typeof g!=="string")return o;var q=[],t,x,C,P,N=true,R=l.isXML(j),Q=g,L;do{d.exec("");if(t=d.exec(Q)){Q=t[3];q.push(t[1]);if(t[2]){P=t[3]; +break}}}while(t);if(q.length>1&&s.exec(g))if(q.length===2&&n.relative[q[0]])x=M(q[0]+q[1],j);else for(x=n.relative[q[0]]?[j]:l(q.shift(),j);q.length;){g=q.shift();if(n.relative[g])g+=q.shift();x=M(g,x)}else{if(!m&&q.length>1&&j.nodeType===9&&!R&&n.match.ID.test(q[0])&&!n.match.ID.test(q[q.length-1])){t=l.find(q.shift(),j,R);j=t.expr?l.filter(t.expr,t.set)[0]:t.set[0]}if(j){t=m?{expr:q.pop(),set:D(m)}:l.find(q.pop(),q.length===1&&(q[0]==="~"||q[0]==="+")&&j.parentNode?j.parentNode:j,R);x=t.expr?l.filter(t.expr, +t.set):t.set;if(q.length>0)C=D(x);else N=false;for(;q.length;){t=L=q.pop();if(n.relative[L])t=q.pop();else L="";if(t==null)t=j;n.relative[L](C,t,R)}}else C=[]}C||(C=x);C||l.error(L||g);if(f.call(C)==="[object Array]")if(N)if(j&&j.nodeType===1)for(g=0;C[g]!=null;g++){if(C[g]&&(C[g]===true||C[g].nodeType===1&&l.contains(j,C[g])))o.push(x[g])}else for(g=0;C[g]!=null;g++)C[g]&&C[g].nodeType===1&&o.push(x[g]);else o.push.apply(o,C);else D(C,o);if(P){l(P,p,o,m);l.uniqueSort(o)}return o};l.uniqueSort=function(g){if(w){h= +k;g.sort(w);if(h)for(var j=1;j0};l.find=function(g,j,o){var m;if(!g)return[];for(var p=0,q=n.order.length;p":function(g,j){var o=typeof j==="string",m,p=0,q=g.length;if(o&&!/\W/.test(j))for(j=j.toLowerCase();p=0))o||m.push(t);else if(o)j[q]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var j=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=j[1]+(j[2]||1)-0;g[3]=j[3]-0}g[0]=e++;return g},ATTR:function(g,j,o, +m,p,q){j=g[1].replace(/\\/g,"");if(!q&&n.attrMap[j])g[1]=n.attrMap[j];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,j,o,m,p){if(g[1]==="not")if((d.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=l(g[3],null,null,j);else{g=l.filter(g[3],j,o,true^p);o||m.push.apply(m,g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled=== +true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,j,o){return!!l(o[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"=== +g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,j){return j===0},last:function(g,j,o,m){return j===m.length-1},even:function(g,j){return j%2===0},odd:function(g,j){return j%2===1},lt:function(g,j,o){return jo[3]-0},nth:function(g,j,o){return o[3]- +0===j},eq:function(g,j,o){return o[3]-0===j}},filter:{PSEUDO:function(g,j,o,m){var p=j[1],q=n.filters[p];if(q)return q(g,o,j,m);else if(p==="contains")return(g.textContent||g.innerText||l.getText([g])||"").indexOf(j[3])>=0;else if(p==="not"){j=j[3];o=0;for(m=j.length;o=0}},ID:function(g,j){return g.nodeType===1&&g.getAttribute("id")===j},TAG:function(g,j){return j==="*"&&g.nodeType===1||g.nodeName.toLowerCase()=== +j},CLASS:function(g,j){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(j)>-1},ATTR:function(g,j){var o=j[1];o=n.attrHandle[o]?n.attrHandle[o](g):g[o]!=null?g[o]:g.getAttribute(o);var m=o+"",p=j[2],q=j[4];return o==null?p==="!=":p==="="?m===q:p==="*="?m.indexOf(q)>=0:p==="~="?(" "+m+" ").indexOf(q)>=0:!q?m&&o!==false:p==="!="?m!==q:p==="^="?m.indexOf(q)===0:p==="$="?m.substr(m.length-q.length)===q:p==="|="?m===q||m.substr(0,q.length+1)===q+"-":false},POS:function(g,j,o,m){var p=n.setFilters[j[2]]; +if(p)return p(g,o,j,m)}}},s=n.match.POS,v=function(g,j){return"\\"+(j-0+1)},B;for(B in n.match){n.match[B]=RegExp(n.match[B].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[B]=RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[B].source.replace(/\\(\d+)/g,v))}var D=function(g,j){g=Array.prototype.slice.call(g,0);if(j){j.push.apply(j,g);return j}return g};try{Array.prototype.slice.call(u.documentElement.childNodes,0)}catch(H){D=function(g,j){var o=j||[],m=0;if(f.call(g)==="[object Array]")Array.prototype.push.apply(o, +g);else if(typeof g.length==="number")for(var p=g.length;m";var o=u.documentElement;o.insertBefore(g,o.firstChild);if(u.getElementById(j)){n.find.ID=function(m,p,q){if(typeof p.getElementById!=="undefined"&&!q)return(p=p.getElementById(m[1]))?p.id===m[1]||typeof p.getAttributeNode!=="undefined"&&p.getAttributeNode("id").nodeValue===m[1]?[p]:A:[]};n.filter.ID=function(m,p){var q=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&q&&q.nodeValue===p}}o.removeChild(g); +o=g=null})();(function(){var g=u.createElement("div");g.appendChild(u.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(j,o){var m=o.getElementsByTagName(j[1]);if(j[1]==="*"){for(var p=[],q=0;m[q];q++)m[q].nodeType===1&&p.push(m[q]);m=p}return m};g.innerHTML="";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(j){return j.getAttribute("href",2)};g=null})();u.querySelectorAll&& +function(){var g=l,j=u.createElement("div");j.innerHTML="

";if(!(j.querySelectorAll&&j.querySelectorAll(".TEST").length===0)){l=function(m,p,q,t){p=p||u;if(!t&&!l.isXML(p))if(p.nodeType===9)try{return D(p.querySelectorAll(m),q)}catch(x){}else if(p.nodeType===1&&p.nodeName.toLowerCase()!=="object"){var C=p.id,P=p.id="__sizzle__";try{return D(p.querySelectorAll("#"+P+" "+m),q)}catch(N){}finally{if(C)p.id=C;else p.removeAttribute("id")}}return g(m,p,q,t)};for(var o in g)l[o]=g[o]; +j=null}}();(function(){var g=u.documentElement,j=g.matchesSelector||g.mozMatchesSelector||g.webkitMatchesSelector||g.msMatchesSelector,o=false;try{j.call(u.documentElement,":sizzle")}catch(m){o=true}if(j)l.matchesSelector=function(p,q){try{if(o||!n.match.PSEUDO.test(q))return j.call(p,q)}catch(t){}return l(q,null,null,[p]).length>0}})();(function(){var g=u.createElement("div");g.innerHTML="
";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length=== +0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(j,o,m){if(typeof o.getElementsByClassName!=="undefined"&&!m)return o.getElementsByClassName(j[1])};g=null}}})();l.contains=u.documentElement.contains?function(g,j){return g!==j&&(g.contains?g.contains(j):true)}:function(g,j){return!!(g.compareDocumentPosition(j)&16)};l.isXML=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false};var M=function(g, +j){for(var o=[],m="",p,q=j.nodeType?[j]:j;p=n.match.PSEUDO.exec(g);){m+=p[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;p=0;for(var t=q.length;p0)for(var h=d;h0},closest:function(a, +b){var d=[],e,f,h=this[0];if(c.isArray(a)){var k={},l,n=1;if(h&&a.length){e=0;for(f=a.length;e-1:c(h).is(e))d.push({selector:l,elem:h,level:n})}h=h.parentNode;n++}}return d}k=$a.test(a)?c(a,b||this.context):null;e=0;for(f=this.length;e-1:c.find.matchesSelector(h,a)){d.push(h);break}else{h=h.parentNode;if(!h|| +!h.ownerDocument||h===b)break}d=d.length>1?c.unique(d):d;return this.pushStack(d,"closest",a)},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var d=typeof a==="string"?c(a,b||this.context):c.makeArray(a),e=c.merge(this.get(),d);return this.pushStack(!d[0]||!d[0].parentNode||d[0].parentNode.nodeType===11||!e[0]||!e[0].parentNode||e[0].parentNode.nodeType===11?e:c.unique(e))},andSelf:function(){return this.add(this.prevObject)}}); +c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling", +d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,e){var f=c.map(this,b,d);Wa.test(a)||(e=d);if(e&&typeof e==="string")f=c.filter(e,f);f=this.length>1?c.unique(f):f;if((this.length>1||Ya.test(e))&&Xa.test(a))f=f.reverse();return this.pushStack(f,a,Za.call(arguments).join(","))}}); +c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return b.length===1?c.find.matchesSelector(b[0],a)?[b[0]]:[]:c.find.matches(a,b)},dir:function(a,b,d){var e=[];for(a=a[b];a&&a.nodeType!==9&&(d===A||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&&e.push(a);a=a[b]}return e},nth:function(a,b,d){b=b||1;for(var e=0;a;a=a[d])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var xa=/ jQuery\d+="(?:\d+|null)"/g, +$=/^\s+/,ya=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,za=/<([\w:]+)/,ab=/\s]+\/)>/g,O={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"], +area:[1,"",""],_default:[0,"",""]};O.optgroup=O.option;O.tbody=O.tfoot=O.colgroup=O.caption=O.thead;O.th=O.td;if(!c.support.htmlSerialize)O._default=[1,"div
","
"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==A)return this.empty().append((this[0]&&this[0].ownerDocument||u).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this, +d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})}, +unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a= +c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,e;(e=this[d])!=null;d++)if(!a||c.filter(a,[e]).length){if(!b&&e.nodeType===1){c.cleanData(e.getElementsByTagName("*")); +c.cleanData([e])}e.parentNode&&e.parentNode.removeChild(e)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild);return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,e=this.ownerDocument;if(!d){d=e.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(xa,"").replace(cb,'="$1">').replace($, +"")],e)[0]}else return this.cloneNode(true)});if(a===true){la(this,b);la(this.find("*"),b.find("*"))}return b},html:function(a){if(a===A)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(xa,""):null;else if(typeof a==="string"&&!Aa.test(a)&&(c.support.leadingWhitespace||!$.test(a))&&!O[(za.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ya,"<$1>");try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?l.cloneNode(true):l)}k.length&&c.each(k,Ka)}return this}});c.buildFragment=function(a,b,d){var e,f,h;b=b&&b[0]?b[0].ownerDocument||b[0]:u;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===u&&!Aa.test(a[0])&&(c.support.checkClone|| +!Ba.test(a[0]))){f=true;if(h=c.fragments[a[0]])if(h!==1)e=h}if(!e){e=b.createDocumentFragment();c.clean(a,b,e,d)}if(f)c.fragments[a[0]]=h?e:1;return{fragment:e,cacheable:f}};c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var e=[];d=c(d);var f=this.length===1&&this[0].parentNode;if(f&&f.nodeType===11&&f.childNodes.length===1&&d.length===1){d[b](this[0]);return this}else{f=0;for(var h= +d.length;f0?this.clone(true):this).get();c(d[f])[b](k);e=e.concat(k)}return this.pushStack(e,a,d.selector)}}});c.extend({clean:function(a,b,d,e){b=b||u;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||u;for(var f=[],h=0,k;(k=a[h])!=null;h++){if(typeof k==="number")k+="";if(k){if(typeof k==="string"&&!bb.test(k))k=b.createTextNode(k);else if(typeof k==="string"){k=k.replace(ya,"<$1>");var l=(za.exec(k)||["",""])[1].toLowerCase(),n=O[l]||O._default, +s=n[0],v=b.createElement("div");for(v.innerHTML=n[1]+k+n[2];s--;)v=v.lastChild;if(!c.support.tbody){s=ab.test(k);l=l==="table"&&!s?v.firstChild&&v.firstChild.childNodes:n[1]===""&&!s?v.childNodes:[];for(n=l.length-1;n>=0;--n)c.nodeName(l[n],"tbody")&&!l[n].childNodes.length&&l[n].parentNode.removeChild(l[n])}!c.support.leadingWhitespace&&$.test(k)&&v.insertBefore(b.createTextNode($.exec(k)[0]),v.firstChild);k=v.childNodes}if(k.nodeType)f.push(k);else f=c.merge(f,k)}}if(d)for(h=0;f[h];h++)if(e&& +c.nodeName(f[h],"script")&&(!f[h].type||f[h].type.toLowerCase()==="text/javascript"))e.push(f[h].parentNode?f[h].parentNode.removeChild(f[h]):f[h]);else{f[h].nodeType===1&&f.splice.apply(f,[h+1,0].concat(c.makeArray(f[h].getElementsByTagName("script"))));d.appendChild(f[h])}return f},cleanData:function(a){for(var b,d,e=c.cache,f=c.event.special,h=c.support.deleteExpando,k=0,l;(l=a[k])!=null;k++)if(!(l.nodeName&&c.noData[l.nodeName.toLowerCase()]))if(d=l[c.expando]){if((b=e[d])&&b.events)for(var n in b.events)f[n]? +c.event.remove(l,n):c.removeEvent(l,n,b.handle);if(h)delete l[c.expando];else l.removeAttribute&&l.removeAttribute(c.expando);delete e[d]}}});var Ca=/alpha\([^)]*\)/i,db=/opacity=([^)]*)/,eb=/-([a-z])/ig,fb=/([A-Z])/g,Da=/^-?\d+(?:px)?$/i,gb=/^-?\d/,hb={position:"absolute",visibility:"hidden",display:"block"},La=["Left","Right"],Ma=["Top","Bottom"],W,ib=u.defaultView&&u.defaultView.getComputedStyle,jb=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){if(arguments.length===2&&b===A)return this; +return c.access(this,a,b,true,function(d,e,f){return f!==A?c.style(d,e,f):c.css(d,e)})};c.extend({cssHooks:{opacity:{get:function(a,b){if(b){var d=W(a,"opacity","opacity");return d===""?"1":d}else return a.style.opacity}}},cssNumber:{zIndex:true,fontWeight:true,opacity:true,zoom:true,lineHeight:true},cssProps:{"float":c.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,d,e){if(!(!a||a.nodeType===3||a.nodeType===8||!a.style)){var f,h=c.camelCase(b),k=a.style,l=c.cssHooks[h];b=c.cssProps[h]|| +h;if(d!==A){if(!(typeof d==="number"&&isNaN(d)||d==null)){if(typeof d==="number"&&!c.cssNumber[h])d+="px";if(!l||!("set"in l)||(d=l.set(a,d))!==A)try{k[b]=d}catch(n){}}}else{if(l&&"get"in l&&(f=l.get(a,false,e))!==A)return f;return k[b]}}},css:function(a,b,d){var e,f=c.camelCase(b),h=c.cssHooks[f];b=c.cssProps[f]||f;if(h&&"get"in h&&(e=h.get(a,true,d))!==A)return e;else if(W)return W(a,b,f)},swap:function(a,b,d){var e={},f;for(f in b){e[f]=a.style[f];a.style[f]=b[f]}d.call(a);for(f in b)a.style[f]= +e[f]},camelCase:function(a){return a.replace(eb,jb)}});c.curCSS=c.css;c.each(["height","width"],function(a,b){c.cssHooks[b]={get:function(d,e,f){var h;if(e){if(d.offsetWidth!==0)h=ma(d,b,f);else c.swap(d,hb,function(){h=ma(d,b,f)});return h+"px"}},set:function(d,e){if(Da.test(e)){e=parseFloat(e);if(e>=0)return e+"px"}else return e}}});if(!c.support.opacity)c.cssHooks.opacity={get:function(a,b){return db.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"": +b?"1":""},set:function(a,b){var d=a.style;d.zoom=1;var e=c.isNaN(b)?"":"alpha(opacity="+b*100+")",f=d.filter||"";d.filter=Ca.test(f)?f.replace(Ca,e):d.filter+" "+e}};if(ib)W=function(a,b,d){var e;d=d.replace(fb,"-$1").toLowerCase();if(!(b=a.ownerDocument.defaultView))return A;if(b=b.getComputedStyle(a,null)){e=b.getPropertyValue(d);if(e===""&&!c.contains(a.ownerDocument.documentElement,a))e=c.style(a,d)}return e};else if(u.documentElement.currentStyle)W=function(a,b){var d,e,f=a.currentStyle&&a.currentStyle[b], +h=a.style;if(!Da.test(f)&&gb.test(f)){d=h.left;e=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;h.left=b==="fontSize"?"1em":f||0;f=h.pixelLeft+"px";h.left=d;a.runtimeStyle.left=e}return f};if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetHeight;return a.offsetWidth===0&&b===0||!c.support.reliableHiddenOffsets&&(a.style.display||c.css(a,"display"))==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var kb=c.now(),lb=/)<[^<]*)*<\/script>/gi, +mb=/^(?:select|textarea)/i,nb=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ob=/^(?:GET|HEAD|DELETE)$/,Na=/\[\]$/,T=/\=\?(&|$)/,ia=/\?/,pb=/([?&])_=[^&]*/,qb=/^(\w+:)?\/\/([^\/?#]+)/,rb=/%20/g,sb=/#.*$/,Ea=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!=="string"&&Ea)return Ea.apply(this,arguments);else if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var f=a.slice(e,a.length);a=a.slice(0,e)}e="GET";if(b)if(c.isFunction(b)){d= +b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);e="POST"}var h=this;c.ajax({url:a,type:e,dataType:"html",data:b,complete:function(k,l){if(l==="success"||l==="notmodified")h.html(f?c("
").append(k.responseText.replace(lb,"")).find(f):k.responseText);d&&h.each(d,[k.responseText,l,k])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&& +!this.disabled&&(this.checked||mb.test(this.nodeName)||nb.test(this.type))}).map(function(a,b){var d=c(this).val();return d==null?null:c.isArray(d)?c.map(d,function(e){return{name:b.name,value:e}}):{name:b.name,value:d}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:e})}, +getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:e})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return new E.XMLHttpRequest},accepts:{xml:"application/xml, text/xml",html:"text/html", +script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},ajax:function(a){var b=c.extend(true,{},c.ajaxSettings,a),d,e,f,h=b.type.toUpperCase(),k=ob.test(h);b.url=b.url.replace(sb,"");b.context=a&&a.context!=null?a.context:b;if(b.data&&b.processData&&typeof b.data!=="string")b.data=c.param(b.data,b.traditional);if(b.dataType==="jsonp"){if(h==="GET")T.test(b.url)||(b.url+=(ia.test(b.url)?"&":"?")+(b.jsonp||"callback")+"=?");else if(!b.data|| +!T.test(b.data))b.data=(b.data?b.data+"&":"")+(b.jsonp||"callback")+"=?";b.dataType="json"}if(b.dataType==="json"&&(b.data&&T.test(b.data)||T.test(b.url))){d=b.jsonpCallback||"jsonp"+kb++;if(b.data)b.data=(b.data+"").replace(T,"="+d+"$1");b.url=b.url.replace(T,"="+d+"$1");b.dataType="script";var l=E[d];E[d]=function(m){f=m;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);if(c.isFunction(l))l(m);else{E[d]=A;try{delete E[d]}catch(p){}}v&&v.removeChild(B)}}if(b.dataType==="script"&&b.cache===null)b.cache= +false;if(b.cache===false&&h==="GET"){var n=c.now(),s=b.url.replace(pb,"$1_="+n);b.url=s+(s===b.url?(ia.test(b.url)?"&":"?")+"_="+n:"")}if(b.data&&h==="GET")b.url+=(ia.test(b.url)?"&":"?")+b.data;b.global&&c.active++===0&&c.event.trigger("ajaxStart");n=(n=qb.exec(b.url))&&(n[1]&&n[1]!==location.protocol||n[2]!==location.host);if(b.dataType==="script"&&h==="GET"&&n){var v=u.getElementsByTagName("head")[0]||u.documentElement,B=u.createElement("script");if(b.scriptCharset)B.charset=b.scriptCharset;B.src= +b.url;if(!d){var D=false;B.onload=B.onreadystatechange=function(){if(!D&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){D=true;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);B.onload=B.onreadystatechange=null;v&&B.parentNode&&v.removeChild(B)}}}v.insertBefore(B,v.firstChild);return A}var H=false,w=b.xhr();if(w){b.username?w.open(h,b.url,b.async,b.username,b.password):w.open(h,b.url,b.async);try{if(b.data!=null&&!k||a&&a.contentType)w.setRequestHeader("Content-Type", +b.contentType);if(b.ifModified){c.lastModified[b.url]&&w.setRequestHeader("If-Modified-Since",c.lastModified[b.url]);c.etag[b.url]&&w.setRequestHeader("If-None-Match",c.etag[b.url])}n||w.setRequestHeader("X-Requested-With","XMLHttpRequest");w.setRequestHeader("Accept",b.dataType&&b.accepts[b.dataType]?b.accepts[b.dataType]+", */*; q=0.01":b.accepts._default)}catch(G){}if(b.beforeSend&&b.beforeSend.call(b.context,w,b)===false){b.global&&c.active--===1&&c.event.trigger("ajaxStop");w.abort();return false}b.global&& +c.triggerGlobal(b,"ajaxSend",[w,b]);var M=w.onreadystatechange=function(m){if(!w||w.readyState===0||m==="abort"){H||c.handleComplete(b,w,e,f);H=true;if(w)w.onreadystatechange=c.noop}else if(!H&&w&&(w.readyState===4||m==="timeout")){H=true;w.onreadystatechange=c.noop;e=m==="timeout"?"timeout":!c.httpSuccess(w)?"error":b.ifModified&&c.httpNotModified(w,b.url)?"notmodified":"success";var p;if(e==="success")try{f=c.httpData(w,b.dataType,b)}catch(q){e="parsererror";p=q}if(e==="success"||e==="notmodified")d|| +c.handleSuccess(b,w,e,f);else c.handleError(b,w,e,p);d||c.handleComplete(b,w,e,f);m==="timeout"&&w.abort();if(b.async)w=null}};try{var g=w.abort;w.abort=function(){w&&g.call&&g.call(w);M("abort")}}catch(j){}b.async&&b.timeout>0&&setTimeout(function(){w&&!H&&M("timeout")},b.timeout);try{w.send(k||b.data==null?null:b.data)}catch(o){c.handleError(b,w,null,o);c.handleComplete(b,w,e,f)}b.async||M();return w}},param:function(a,b){var d=[],e=function(h,k){k=c.isFunction(k)?k():k;d[d.length]=encodeURIComponent(h)+ +"="+encodeURIComponent(k)};if(b===A)b=c.ajaxSettings.traditional;if(c.isArray(a)||a.jquery)c.each(a,function(){e(this.name,this.value)});else for(var f in a)ca(f,a[f],b,e);return d.join("&").replace(rb,"+")}});c.extend({active:0,lastModified:{},etag:{},handleError:function(a,b,d,e){a.error&&a.error.call(a.context,b,d,e);a.global&&c.triggerGlobal(a,"ajaxError",[b,a,e])},handleSuccess:function(a,b,d,e){a.success&&a.success.call(a.context,e,d,b);a.global&&c.triggerGlobal(a,"ajaxSuccess",[b,a])},handleComplete:function(a, +b,d){a.complete&&a.complete.call(a.context,b,d);a.global&&c.triggerGlobal(a,"ajaxComplete",[b,a]);a.global&&c.active--===1&&c.event.trigger("ajaxStop")},triggerGlobal:function(a,b,d){(a.context&&a.context.url==null?c(a.context):c.event).trigger(b,d)},httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),e=a.getResponseHeader("Etag"); +if(d)c.lastModified[b]=d;if(e)c.etag[b]=e;return a.status===304},httpData:function(a,b,d){var e=a.getResponseHeader("content-type")||"",f=b==="xml"||!b&&e.indexOf("xml")>=0;a=f?a.responseXML:a.responseText;f&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&e.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&e.indexOf("javascript")>=0)c.globalEval(a);return a}});if(E.ActiveXObject)c.ajaxSettings.xhr= +function(){if(E.location.protocol!=="file:")try{return new E.XMLHttpRequest}catch(a){}try{return new E.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}};c.support.ajax=!!c.ajaxSettings.xhr();var da={},tb=/^(?:toggle|show|hide)$/,ub=/^([+\-]=)?([\d+.\-]+)(.*)$/,aa,na=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b,d){if(a||a===0)return this.animate(S("show",3),a,b,d);else{a= +0;for(b=this.length;a=0;e--)if(d[e].elem===this){b&&d[e](true);d.splice(e,1)}});b||this.dequeue();return this}});c.each({slideDown:S("show",1),slideUp:S("hide",1),slideToggle:S("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,e,f){return this.animate(b, +d,e,f)}});c.extend({speed:function(a,b,d){var e=a&&typeof a==="object"?c.extend({},a):{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};e.duration=c.fx.off?0:typeof e.duration==="number"?e.duration:e.duration in c.fx.speeds?c.fx.speeds[e.duration]:c.fx.speeds._default;e.old=e.complete;e.complete=function(){e.queue!==false&&c(this).dequeue();c.isFunction(e.old)&&e.old.call(this)};return e},easing:{linear:function(a,b,d,e){return d+e*a},swing:function(a,b,d,e){return(-Math.cos(a* +Math.PI)/2+0.5)*e+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a=parseFloat(c.css(this.elem,this.prop));return a&&a>-1E4?a:0},custom:function(a,b,d){function e(h){return f.step(h)} +this.startTime=c.now();this.connect=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.connect;this.pos=this.state=0;var f=this;a=c.fx;e.elem=this.elem;if(e()&&c.timers.push(e)&&!aa)aa=setInterval(a.tick,a.interval)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true; +this.custom(this.cur(),0)},step:function(a){var b=c.now(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var e in this.options.curAnim)if(this.options.curAnim[e]!==true)d=false;if(d){if(this.options.overflow!=null&&!c.support.shrinkWrapBlocks){var f=this.elem,h=this.options;c.each(["","X","Y"],function(l,n){f.style["overflow"+n]=h.overflow[l]})}this.options.hide&&c(this.elem).hide();if(this.options.hide|| +this.options.show)for(var k in this.options.curAnim)c.style(this.elem,k,this.options.orig[k]);this.options.complete.call(this.elem)}return false}else{a=b-this.startTime;this.state=a/this.options.duration;b=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||b](this.state,a,0,1,this.options.duration);this.now=this.connect+(this.end-this.connect)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a= +c.timers,b=0;b-1;e={};var s={};if(n)s=f.position();k=n?s.top:parseInt(k,10)||0;l=n?s.left:parseInt(l,10)||0;if(c.isFunction(b))b=b.call(a,d,h);if(b.top!=null)e.top=b.top-h.top+k;if(b.left!=null)e.left=b.left-h.left+l;"using"in b?b.using.call(a, +e):f.css(e)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),e=Fa.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.css(a,"marginTop"))||0;d.left-=parseFloat(c.css(a,"marginLeft"))||0;e.top+=parseFloat(c.css(b[0],"borderTopWidth"))||0;e.left+=parseFloat(c.css(b[0],"borderLeftWidth"))||0;return{top:d.top-e.top,left:d.left-e.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||u.body;a&&!Fa.test(a.nodeName)&& +c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(e){var f=this[0],h;if(!f)return null;if(e!==A)return this.each(function(){if(h=ea(this))h.scrollTo(!a?e:c(h).scrollLeft(),a?e:c(h).scrollTop());else this[d]=e});else return(h=ea(f))?"pageXOffset"in h?h[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&h.document.documentElement[d]||h.document.body[d]:f[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase(); +c.fn["inner"+b]=function(){return this[0]?parseFloat(c.css(this[0],d,"padding")):null};c.fn["outer"+b]=function(e){return this[0]?parseFloat(c.css(this[0],d,e?"margin":"border")):null};c.fn[d]=function(e){var f=this[0];if(!f)return e==null?null:this;if(c.isFunction(e))return this.each(function(h){var k=c(this);k[d](e.call(this,h,k[d]()))});return c.isWindow(f)?f.document.compatMode==="CSS1Compat"&&f.document.documentElement["client"+b]||f.document.body["client"+b]:f.nodeType===9?Math.max(f.documentElement["client"+ +b],f.body["scroll"+b],f.documentElement["scroll"+b],f.body["offset"+b],f.documentElement["offset"+b]):e===A?parseFloat(c.css(f,d)):this.css(d,typeof e==="string"?e:e+"px")}})})(window); diff --git a/sitebricks-web/src/main/resources/js/json2.js b/sitebricks-web/src/main/resources/js/json2.js new file mode 100644 index 00000000..317d47ab --- /dev/null +++ b/sitebricks-web/src/main/resources/js/json2.js @@ -0,0 +1,324 @@ +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (!this.JSON) { + this.JSON = {}; +} + +(function () { + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return isFinite(this.valueOf()) ? + this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + k = rep[i]; + if (typeof k === 'string') { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : + gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ +.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') +.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') +.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); diff --git a/sitebricks-web/src/main/resources/js/rpc.js b/sitebricks-web/src/main/resources/js/rpc.js new file mode 100644 index 00000000..41003a6a --- /dev/null +++ b/sitebricks-web/src/main/resources/js/rpc.js @@ -0,0 +1,41 @@ +/** + * @fileoverview Client/Server RPC encapsulation. Relies on the presence + * of jQuery 1.4.3. + * Classes: + * sitebricks.Rpc + */ + +var sitebricks = sitebricks || {}; + + +/** + * Encapsulates client/server RPCs. + * + * @constructor + */ +sitebricks.Rpc = function() { +}; + +/** + * Underlying RPC dispatch function using Ajax. Uses Json batching. + * + * @export + * @param args Rpc request data. + */ +sitebricks.Rpc.prototype.rpc = function(args, opt_callback) { + var url = '/ajax/' + args.rpc; + var self = this; + args = { data: JSON.stringify(args) }; + opt_callback = opt_callback || function() {}; + + $.ajax({ + type: 'POST', + url: url, + dataType: 'json', + data: args, + success: opt_callback, + failure: function() { + alert("Failed to contact server =("); + } + }); +}; diff --git a/sitebricks-web/src/main/resources/js/sitebricks.js b/sitebricks-web/src/main/resources/js/sitebricks.js new file mode 100644 index 00000000..44a3928f --- /dev/null +++ b/sitebricks-web/src/main/resources/js/sitebricks.js @@ -0,0 +1,46 @@ +/** + * Sitebricks.info Website. + */ +$(function() { + var rpc = new sitebricks.Rpc(); + + // Open a new document in place. + $('a.wikilink').live('click', function() { + var name = $(this).attr('name'); + rpc.rpc({ rpc: 'page/' + name }, function(data) { + $('#main article').html(data); + }); + }); + + + // Edit/delete controls. + $('#edit').click(function() { + var name = $('#main article').attr('name'); + $('nav#controls .view').hide(); + $('nav#controls .edit').show(); + $('#editor').fadeIn(); + $('#editor article').text(''); + + rpc.rpc({ rpc: 'markdown/' + name }, function(data) { + var article = $('#editor article'); + article.text(data); + article.focus(); + }); + }); + + // Save/Discard controls. + $('#save').click(function() { + $('#editor').fadeOut('fast'); + $('nav#controls .edit').hide(); + $('nav#controls .view').show(); + + var name = $('#main article').attr('name'); + rpc.rpc({ rpc: 'save/' + name, text: $('#editor article').text() }); + }); + + $('#discard').click(function() { + $('#editor').fadeOut('fast'); + $('nav#controls .edit').hide(); + $('nav#controls .view').show(); + }); +}); diff --git a/sitebricks-web/src/main/resources/main.css b/sitebricks-web/src/main/resources/main.css new file mode 100644 index 00000000..810f8545 --- /dev/null +++ b/sitebricks-web/src/main/resources/main.css @@ -0,0 +1,255 @@ +body { + font-family: "Helvetica neue", "Helvetica", sans-serif; + font-size: 16px; + font-weight: lighter; + line-height: 1; + margin: 0; + background: #e4e2e2; +} + +h1, h2 { + font-family: "Helvetica neue", "Helvetica", sans-serif; + font-weight: lighter; +} + +/** header **/ +h1 { + margin: 8px; + padding: 0 14px; +} + +h1 > span { + font-size: 60%; + color: #666; +} + +header { + height: 40px; +} + +header div.groove { + border-top: 1px solid #aaa; + border-bottom: 1px solid #eee; +} + +header nav { + position: absolute; + left: 220px; +} + +header nav ul { + position: relative; + top: -8px; + list-style-type: none; +} + +header nav ul li:hover { + background: #9f9f9f; +} + +header nav ul li { + float: left; + margin-right: 10px; + background: #cfcfcf; + -webkit-border-radius: 5px; + padding: 5px 8px; + font-size: 90%; + line-height: 0.8; + color: white; + cursor: pointer; +} + +input.searchbox { + position: absolute; + padding: 3px 8px; + left: 640px; + margin-top: 8px; + + width: 220px; + outline: none; + -webkit-border-radius: 22px; + border: 1px solid #aaa; +} + +/** left nav **/ +nav#left { + position: absolute; + width: 19%; + + font-size: 14px; +} + +nav#left ul { + margin-left: 10px; + margin-top: 28px; + list-style-type: none; +} + +nav#left ul a:hover { + text-decoration: underline; +} + +nav#left ul a { + text-decoration: none; + color: #993300; + padding: 4px 8px; + -webkit-border-radius: 4px; +} + +nav#left ul li:first-child { + margin-bottom: 22px; +} + +nav#left ul li.back { + margin-left: -19px; +} + +nav#left ul li { + margin-bottom: 6px; +} + +/** content section **/ +h2 { + padding: 10px 14px; + font-size: 20px; + background: #383838; + color: #eee; + text-shadow: 1px 1px 1px #666; + -webkit-border-radius: 10px; +} + +h3 { + margin-top: 28px; + padding-left: 4px; + padding-right: 4px; + padding-bottom: 4px; + font-size: 18px; + font-weight: lighter; + border-bottom: 1px solid #888; +} + +h4 { + padding: 0 4px; + margin-top: 24px; + margin-bottom: 16px; + font-size: 16px; + font-weight: normal; +} + +section#main { + position: absolute; + left: 258px; + right: 0; + bottom: 0; + top: 49px; + overflow-y: auto; +} + +article { + margin-top: 20px; + width: 620px; +} + +article a { + color: #888; + text-decoration: none; +} + +article a:hover { + text-decoration: underline; +} + +article p { + line-height: 1.3; + padding: 4px 8px; /*text-shadow: 1px 1px 1px #fff;*/ +} + +article pre { + margin: 0 6px; + font-family: "Courier New", monospace; + font-size: 90%; + font-weight: lighter; + background: #fff; + padding: 12px; + -webkit-border-radius: 6px; +} + +pre b { + font-weight: bold; + color: #2D8DD5; +} + +article header { + float: right; + color: #ccc; + margin-top: 14px; + margin-right: 13px; + font-style: italic; + font-size: 12px; +} + +article header span.author { + font-weight: normal; +} + +/** General action button **/ +button:hover { + box-shadow: 0 0 6px #fff; +} + +button { + cursor: pointer; + padding: 3px 8px; + color: white; + border: none; + + font-weight: bold; + text-shadow: 0px 1px 1px #aaa; + /*box-shadow: 0 1px 2px #666;*/ + box-shadow: 0 0px 2px #222; + -webkit-border-radius: 4px; + background: -webkit-gradient( + linear, + left top, + left bottom, + /*from(#409FE8),*/ + from(#93DDFD), + to(#2380c4) + ); +} + +/** Controls menu **/ +nav#controls { + position: absolute; + top: 68px; + left: 920px; + z-index: 999; +} + +nav#controls .edit { + display: none; +} + + +/** Overlay editor **/ +#editor { + display: none; + position: absolute; + z-index: 1000; + + top: 126px; + left: 240px; + width: 660px; + height: 80%; + overflow-y: auto; + overflow-x: hidden; + background: #e4e2e2; + + box-shadow: 2px 4px 12px #555; + -webkit-border-radius: 20px; +} + +#editor article { + outline: none; + padding: 12px 28px 12px 18px; +} \ No newline at end of file diff --git a/sitebricks/TODO b/sitebricks/TODO new file mode 100644 index 00000000..a9d3ff3d --- /dev/null +++ b/sitebricks/TODO @@ -0,0 +1,18 @@ +TODO +---- + +- Fix all error checking +- Make it possible to weaken the binding target error checking +- Make it possible to turn off generic checking (or fix it) +- RPC batching +- Cometd support (hanging get/websocket) +- Fix up template error reporting +x add patch for script from Jared +x Inject anything into method +- Create Request abstraction for @Services + +More generifiable stuff: +- timing module +x Monitoring module (statusz, varz etc.) +- General RPC system? +- Async HTTP client diff --git a/sitebricks/pom.atom b/sitebricks/pom.atom new file mode 100644 index 00000000..dc7a29d9 --- /dev/null +++ b/sitebricks/pom.atom @@ -0,0 +1,23 @@ +# Sitebricks Maven Atom buildfile + +repositories << "http://repo1.maven.org/maven2" + +project "Google Sitebricks" @ "http://code.google.com/p/google-sitebricks" + id: com.google.sitebricks:sitebricks:0.8-SNAPSHOT + deps: [ org.mvel:mvel2:2.0.16 + com.google.inject:guice:2.0 + com.google.inject.extensions:guice-servlet:2.0 + com.google.collections:google-collections:1.0-rc2 + net.jcip:jcip-annotations:1.0 + com.intellij:annotations:7.0.3 + commons-httpclient:commons-httpclient:3.1 + commons-io:commons-io:1.4 + jaxen:jaxen:1.1.1 + saxpath:saxpath:1.0-FCS + javax.servlet:servlet-api:2.5 + xstream:xstream:1.2.2 + org.easymock:easymock:2.4 + org.testng:testng:5.8(jdk15) + org.codehaus.jackson:jackson-core-asl:1.3.2 + org.codehaus.jackson:jackson-mapper-asl:1.3.2 + org.jsoup:jsoup:1.2.3 ] diff --git a/sitebricks/pom.xml b/sitebricks/pom.xml new file mode 100644 index 00000000..9df036e3 --- /dev/null +++ b/sitebricks/pom.xml @@ -0,0 +1,140 @@ + + 4.0.0 + + com.google.sitebricks + sitebricks-parent + 0.8.5 + + sitebricks + Sitebricks :: Core + + + + org.testng + testng + ${org.testng.version} + jdk15 + test + + + com.google.sitebricks + sitebricks-converter + + + com.google.sitebricks + sitebricks-client + + + org.mvel + mvel2 + + + com.google.guava + guava + + + net.jcip + jcip-annotations + + + com.intellij + annotations + + + com.google.inject + guice + + + com.google.inject.extensions + guice-servlet + + + com.google.inject.extensions + guice-multibindings + + + com.ning + async-http-client + + + commons-io + commons-io + + + commons-lang + commons-lang + + + dom4j + dom4j + + + xml-apis + xml-apis + + + + + jaxen + jaxen + + + xercesImpl + xerces + + + xml-apis + xml-apis + + + + + saxpath + saxpath + + + javax.servlet + servlet-api + + + com.thoughtworks.xstream + xstream + + + org.easymock + easymock + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-mapper-asl + + + + org.jsoup + jsoup + + + org.freemarker + freemarker + + + + + + + google-snapshots + Sonatype OSS Nexus Snapshots + https://oss.sonatype.org/content/repositories/google-snapshots + + + google-with-staging + Nexus OSS Staging Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + diff --git a/sitebricks/src/main/java/com/google/sitebricks/ActionDescriptor.java b/sitebricks/src/main/java/com/google/sitebricks/ActionDescriptor.java new file mode 100644 index 00000000..01c9f8e9 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/ActionDescriptor.java @@ -0,0 +1,94 @@ +package com.google.sitebricks; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.inject.Key; +import com.google.sitebricks.routing.Action; + +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Describes an action binding in the SPI for actions. + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class ActionDescriptor implements PageBinder.ActionBinder { + private final Action action; + private final Key actionKey; + + private final Map selectParams = Maps.newHashMap(); + private final Map selectHeaders = Maps.newHashMap(); + + private final Set> methods = Sets.newHashSet(); + + // Pass thru for builder config. + private final PageBinder.PerformBinder performBinder; + + public ActionDescriptor(Action action, PageBinder.PerformBinder performBinder) { + this.action = action; + this.performBinder = performBinder; + this.actionKey = null; + } + + public ActionDescriptor(Key action, PageBinder.PerformBinder performBinder) { + this.performBinder = performBinder; + this.action = null; + this.actionKey = action; + } + + @Override + public PageBinder.PerformBinder on(Class... method) { + Preconditions.checkArgument(null != method && method.length > 0, + "Must specify at least one method"); + methods.addAll(Sets.newHashSet(method)); + return performBinder; + } + + @Override + public PageBinder.ActionBinder select(String param, String value) { + Preconditions.checkArgument(param != null && !param.isEmpty(), + "Parameter to select() must be a non-empty string"); + Preconditions.checkArgument(value != null && !value.isEmpty(), + "Value to select() must be a non-empty string"); + selectParams.put(param, value); + return this; + } + + @Override + public PageBinder.ActionBinder selectHeader(String header, String value) { + Preconditions.checkArgument(header != null && !header.isEmpty(), + "Header to selectHeader() must be a non-empty string"); + Preconditions.checkArgument(value != null && !value.isEmpty(), + "Value to selectHeader() must be a non-empty string"); + selectHeaders.put(header, value); + return this; + } + + @Override + public PageBinder.ActionBinder selectHeader(String param, Pattern regex) { + throw new UnsupportedOperationException("To be implemented"); + } + + public Action getAction() { + return action; + } + + public Key getActionKey() { + return actionKey; + } + + public Map getSelectParams() { + return selectParams; + } + + public Map getSelectHeaders() { + return selectHeaders; + } + + public Set> getMethods() { + return methods; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/At.java b/sitebricks/src/main/java/com/google/sitebricks/At.java new file mode 100644 index 00000000..55a4aea1 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/At.java @@ -0,0 +1,15 @@ +package com.google.sitebricks; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface At { + String value(); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/Aware.java b/sitebricks/src/main/java/com/google/sitebricks/Aware.java new file mode 100644 index 00000000..58111985 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/Aware.java @@ -0,0 +1,10 @@ +package com.google.sitebricks; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public interface Aware { + void startup(); + + void shutdown(); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/AwareModule.java b/sitebricks/src/main/java/com/google/sitebricks/AwareModule.java new file mode 100644 index 00000000..fbc17b81 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/AwareModule.java @@ -0,0 +1,26 @@ +package com.google.sitebricks; + +import com.google.common.base.Preconditions; +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.binder.ScopedBindingBuilder; +import com.google.inject.name.Names; + +import java.util.UUID; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public abstract class AwareModule extends AbstractModule { + @Override + protected final void configure() { + configureLifecycle(); + } + + protected abstract void configureLifecycle(); + + protected ScopedBindingBuilder observe(Class aware) { + Preconditions.checkArgument(!Aware.class.equals(aware), "Can't bind to interface Aware"); + return bind(Key.get(Aware.class, Names.named(UUID.randomUUID().toString()))).to(aware); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/Bootstrapper.java b/sitebricks/src/main/java/com/google/sitebricks/Bootstrapper.java new file mode 100644 index 00000000..f04f9510 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/Bootstrapper.java @@ -0,0 +1,16 @@ +package com.google.sitebricks; + +import com.google.inject.ImplementedBy; +import com.google.inject.TypeLiteral; + +/** + * An internal hook to start the Sitebricks application lifecycle. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@ImplementedBy(ScanAndCompileBootstrapper.class) +interface Bootstrapper { + TypeLiteral AWARE_TYPE = new TypeLiteral(){}; + + void start(); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/Bricks.java b/sitebricks/src/main/java/com/google/sitebricks/Bricks.java new file mode 100644 index 00000000..f2559020 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/Bricks.java @@ -0,0 +1,14 @@ +package com.google.sitebricks; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@Retention(RetentionPolicy.RUNTIME) +@BindingAnnotation +public @interface Bricks { +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/Classes.java b/sitebricks/src/main/java/com/google/sitebricks/Classes.java new file mode 100644 index 00000000..296613d0 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/Classes.java @@ -0,0 +1,186 @@ +package com.google.sitebricks; + + +import com.google.inject.matcher.Matcher; +import net.jcip.annotations.Immutable; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Logger; + + +/** + * Utility class that finds all the classes in a given package. + * (based on a similar utility in TestNG) + *

+ * Created on Feb 24, 2006 + * + * @author Cedric Beust + * @author Dhanji R. Prasanna (dhanji@gmail.com) + * @see org.testng.internal.PackageUtils + */ +@Immutable +class Classes { + + private final Matcher> matcher; + private final Logger log = Logger.getLogger(Classes.class.getName()); + + private Classes(Matcher> matcher) { + this.matcher = matcher; + } + + /** + * @param pack A list of packages to scan, recursively. + * @return A set of all the classes inside this package + * @throws PackageScanFailedException Thrown when error reading from disk or jar. + * @throws IllegalStateException Thrown when something very odd is + * happening with classloaders. + */ + @NotNull + public Set> in(Package pack) { + String packageName = pack.getName(); + String packageOnly = pack.getName(); + + final boolean recursive = true; + + Set> classes = new LinkedHashSet>(); + String packageDirName = packageOnly.replace('.', '/'); + + Enumeration dirs; + try { + dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName); + + } catch (IOException e) { + throw new PackageScanFailedException( + "Could not read from package directory: " + packageDirName, e); + } + + while (dirs.hasMoreElements()) { + URL url = dirs.nextElement(); + String protocol = url.getProtocol(); + + if ("file".equals(protocol)) { + findClassesInDirPackage(packageOnly, toPath(url), recursive, classes); + } else if ("jar".equals(protocol)) { + JarFile jar; + + try { + jar = ((JarURLConnection) url.openConnection()).getJarFile(); + } catch (IOException e) { + throw new PackageScanFailedException("Could not read from jar url: " + url, e); + } + + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + if (name.charAt(0) == '/') { + name = name.substring(1); + } + if (name.startsWith(packageDirName)) { + int idx = name.lastIndexOf('/'); + if (idx != -1) { + packageName = name.substring(0, idx).replace('/', '.'); + } + + if ((idx != -1) || recursive) { + //it's not inside a deeper dir + if (name.endsWith(".class") && !entry.isDirectory()) { + String className = name.substring(packageName.length() + 1, name.length() - 6); + //include this class in our results + + /* Issue #26 - package-info causes ISE during package scan. This ia a bit of a hack for just + * package-info. TODO Determine better handling of unexpected classloader issues. + */ + if (!"package-info".equalsIgnoreCase(className)) { + add(packageName, classes, className); + } +// vResult.add(); + } + } + } + } + } + } + + return classes; + } + + private void add(String packageName, Set> classes, String className) { + + Class clazz; + try { + clazz = Class.forName(packageName + '.' + className); + } catch (ClassNotFoundException e) { + log.severe("A class discovered by the scanner could not be found by the ClassLoader, " + + "something very odd has happened with the classloading (see root cause): " + + e.toString()); + + throw new IllegalStateException( + "A class discovered by the scanner could not be found by the ClassLoader", e); + } + + if (matcher.matches(clazz)) + classes.add(clazz); + } + + private void findClassesInDirPackage(String packageName, + String packagePath, + final boolean recursive, + Set> classes) { + File dir = new File(packagePath); + + if (!dir.exists() || !dir.isDirectory()) { + return; + } + + File[] dirfiles = dir.listFiles(new FileFilter() { + public boolean accept(File file) { + return (recursive && file.isDirectory()) || (file.getName().endsWith(".class")); + } + }); + + for (File file : dirfiles) { + if (file.isDirectory()) { + findClassesInDirPackage(packageName + "." + file.getName(), + file.getAbsolutePath(), + recursive, + classes); + } else { + String className = file.getName().substring(0, file.getName().length() - 6); + //include class + add(packageName, classes, className); + } + } + } + + public static Classes matching(Matcher> matcher) { + return new Classes(matcher); + } + + private static String toPath(final URL url) { + String path = url.getPath(); + StringBuilder buf = new StringBuilder(); + for ( int i = 0, length = path.length(); i < length; i++ ) { + char c = path.charAt( i ); + if ( '/' == c ) { + buf.append( File.separatorChar ); + } else if ( '%' == c && i < length - 2 ) { + buf.append( (char) Integer.parseInt( path.substring( ++i, ++i + 1 ), 16 ) ); + } else { + buf.append( c ); + } + } + return buf.toString(); + } + +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/DebugModePageBook.java b/sitebricks/src/main/java/com/google/sitebricks/DebugModePageBook.java new file mode 100644 index 00000000..c1a25bd0 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/DebugModePageBook.java @@ -0,0 +1,125 @@ +package com.google.sitebricks; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.inject.servlet.RequestScoped; +import com.google.sitebricks.compiler.Compilers; +import com.google.sitebricks.routing.PageBook; +import com.google.sitebricks.routing.Production; +import com.google.sitebricks.routing.SystemMetrics; +import net.jcip.annotations.ThreadSafe; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Used in the development stage to intercept the real pagebook so we can reload + * & recompile templates on demand. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@ThreadSafe +@Singleton +class DebugModePageBook implements PageBook { + private final PageBook book; + private final SystemMetrics metrics; + private final Compilers compilers; + private final Provider memo; + + @Inject + public DebugModePageBook(@Production PageBook book, + SystemMetrics metrics, Compilers compilers, + Provider memo) { + this.book = book; + this.metrics = metrics; + this.compilers = compilers; + this.memo = memo; + } + + public Page at(String uri, Class myPageClass) { + return book.at(uri, myPageClass); + } + + public Page get(String uri) { + final Page page = book.get(uri); + + //reload template + reload(uri, page); + + return page; + } + + public Page forName(String name) { + final Page page = book.forName(name); + + //reload template + reload(name, page); + + return page; + } + + public Page embedAs(Class page, String as) { + return book.embedAs(page, as); + } + + @Override + public Page decorate(Class pageClass) { + return book.decorate(pageClass); + } + + public Page nonCompilingGet(String uri) { + // Simply delegate thru to the real page book. + return book.get(uri); + } + + public Page forInstance(Object instance) { + return book.forInstance(instance); + } + + public Page forClass(Class pageClass) { + return book.forClass(pageClass); + } + + public Page serviceAt(String uri, Class pageClass) { + return book.serviceAt(uri, pageClass); + } + + public Collection> getPageMap() { + return book.getPageMap(); + } + + @Override + public void at(String uri, List actionDescriptor, + Map, String> methodSet) { + book.at(uri, actionDescriptor, methodSet); + } + + private void reload(String identifier, Page page) { + + // Do nothing on the first pass since the page is already compiled. + // Also skips static resources and headless web services. + if (null == page || !metrics.isActive() || page.isHeadless()) + return; + + // Ensure we reload only once per request, per identifier. + final Memo memo = this.memo.get(); + if (memo.uris.contains(identifier)) + return; + + // Otherwise, remember that we already loaded it in this request. + memo.uris.add(identifier); + + // load template and compile + compilers.compilePage(page); + } + + @RequestScoped + private static class Memo { + private final Set uris = new HashSet(); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/DebugModeRoutingDispatcher.java b/sitebricks/src/main/java/com/google/sitebricks/DebugModeRoutingDispatcher.java new file mode 100644 index 00000000..b288076d --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/DebugModeRoutingDispatcher.java @@ -0,0 +1,125 @@ +package com.google.sitebricks; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.sitebricks.compiler.TemplateCompileException; +import com.google.sitebricks.routing.PageBook; +import com.google.sitebricks.routing.Production; +import com.google.sitebricks.routing.RoutingDispatcher; +import com.google.sitebricks.routing.SystemMetrics; +import net.jcip.annotations.ThreadSafe; +import org.mvel2.PropertyAccessException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; + +/** + * In debug mode, this dispatcher is used to intercept the production dispatcher and provide debug + * services (such as the /debug page, and the friendly compile errors page). + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@ThreadSafe +@Singleton +class DebugModeRoutingDispatcher implements RoutingDispatcher { + private final RoutingDispatcher dispatcher; + private final SystemMetrics metrics; + private final PageBook pageBook; + private final Provider respondProvider; + + @Inject + public DebugModeRoutingDispatcher(@Production RoutingDispatcher dispatcher, + SystemMetrics metrics, + PageBook pageBook, + Provider respondProvider) { + + this.dispatcher = dispatcher; + this.metrics = metrics; + this.pageBook = pageBook; + this.respondProvider = respondProvider; + } + + + public Respond dispatch(HttpServletRequest request, HttpServletResponse response) + throws IOException { + long start = System.currentTimeMillis(); + + // Attempt to discover page class. + final PageBook.Page page = pageBook.get(request + .getRequestURI() + .substring(request.getContextPath().length())); + + // This may be a static resource (in which case we dont gather metrics for it). + Class pageClass = null; + if (null != page) + pageClass = page.pageClass(); + + try { + return dispatcher.dispatch(request, response); + + + } catch (TemplateCompileException tce) { + // NOTE(dhanji): Don't log error metrics here, they are better handled by the compiler. + + final Respond respond = respondProvider.get(); + + respond.write("

"); + respond.write("Compile errors in page"); + respond.write("

"); + respond.write("
");
+      respond.write(tce.getMessage());
+      respond.write("
"); + respond.write("
"); + respond.write("
"); + respond.write("
"); + + return respond; + + + } catch (PropertyAccessException pae) { + final Respond respond = respondProvider.get(); + + Throwable cause = pae.getCause(); + + respond.write("

"); + respond.write("Exception during page render"); + respond.write("

"); + respond.write("
"); + respond.write("
"); + respond.write("
"); + + // Analyze cause and construct a detailed error report. + if (cause instanceof InvocationTargetException) { + InvocationTargetException ite = (InvocationTargetException) cause; + cause = ite.getCause(); + } + + if (cause == null) + cause = pae; + + // Create ourselves a printwriter to buffer error output into. + final StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + + respond.write("

"); + respond.write("Exception during page render"); + respond.write("

"); + respond.write("
");
+      respond.write(writer.toString());
+      respond.write("
"); + + return respond; + } finally { + long time = System.currentTimeMillis() - start; + + // Only log time metric if this is a dynamic resource. + if (null != pageClass) + metrics.logPageRenderTime(pageClass, time); + } + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/Evaluator.java b/sitebricks/src/main/java/com/google/sitebricks/Evaluator.java new file mode 100644 index 00000000..0c11e2cb --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/Evaluator.java @@ -0,0 +1,17 @@ +package com.google.sitebricks; + +import com.google.inject.ImplementedBy; +import org.jetbrains.annotations.Nullable; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ImplementedBy(MvelEvaluator.class) +public interface Evaluator { + @Nullable + Object evaluate(String expr, Object bean); + + void write(String expr, Object bean, Object value); + + Object read(String property, Object contextObject); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/Export.java b/sitebricks/src/main/java/com/google/sitebricks/Export.java new file mode 100644 index 00000000..5819c1ab --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/Export.java @@ -0,0 +1,25 @@ +package com.google.sitebricks; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * TODO Maybe deprecate this and force resources to be explicitly bound. + * + *

Mark classes with this to configure loading static resource from that + * class's neighborhood (i.e. using {@code Class.getResourceAsStream}). Example: + *

+ * {@literal @}Export(at="/my.js", resource="my.js")
+ * public class MyWebPage { .. }
+ *
+ * @author Dhanji R. Prasanna (dhanji@gmail com)
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Export {
+  String at();
+
+  String resource();
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/GaeModule.java b/sitebricks/src/main/java/com/google/sitebricks/GaeModule.java
new file mode 100644
index 00000000..629f1f63
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/GaeModule.java
@@ -0,0 +1,23 @@
+package com.google.sitebricks;
+
+import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
+import com.google.sitebricks.binding.FlashCache;
+import com.google.sitebricks.binding.GaeFlashCache;
+
+/**
+ * Sitebricks additional configuration module to work properly in Google Appengine.
+ *
+ * @author dhanji@gmail.com (Dhanji R. Prasanna)
+ */
+public class GaeModule extends ServletModule {
+
+  @Override
+  protected void configureServlets() {
+    bind(FlashCache.class).to(GaeFlashCache.class).in(Singleton.class);
+
+    // Mvel's JIT produces weird security exceptions in GAE because of its flagrant use
+    // of sun.misc.Unsafe
+    System.setProperty("mvel2.disable.jit", "true");
+  }
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/HiddenMethodFilter.java b/sitebricks/src/main/java/com/google/sitebricks/HiddenMethodFilter.java
new file mode 100644
index 00000000..8c31ddc2
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/HiddenMethodFilter.java
@@ -0,0 +1,101 @@
+
+package com.google.sitebricks;
+
+import com.google.inject.Singleton;
+import com.google.sitebricks.rendering.Strings;
+import net.jcip.annotations.Immutable;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * Enables browsers making a simulated PUT and DELETE requests. Currently browsers support making
+ * GET and POST requests. This {@link javax.servlet.Filter} checks if a hidden field is set and
+ * renames HTTP method, retrieved via {@link javax.servlet.http.HttpServletRequest#getMethod()} to a method
+ * set in the hidden field
+ *
+ * @author Peter Knego
+ */
+@Immutable
+@Singleton
+class HiddenMethodFilter implements Filter {
+
+  private static final String FILTER_DONE_SUFFIX = "__done";
+  private static final String HIDDEN_FIELD_NAME = "hiddenFieldName";
+
+  /**
+   * Name of the hidden field.
+   */
+  private String hiddenFieldName = "__sitebricks__action";
+  private String filterDoneAttributeName;
+
+  public void init(FilterConfig filterConfig) throws ServletException {
+    String param = filterConfig.getInitParameter(HIDDEN_FIELD_NAME);
+    if (param != null) {
+      hiddenFieldName = param;
+    }
+
+    // Request attribute name to signal that filtering was already done in a request
+    filterDoneAttributeName = hiddenFieldName + FILTER_DONE_SUFFIX;
+  }
+
+
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
+      throws IOException, ServletException {
+
+    HttpServletRequest httpRequest = (HttpServletRequest) request;
+
+    // check if filtering was already done on this request
+    if (httpRequest.getAttribute(filterDoneAttributeName) != null) {
+      // Filtering done, forward to another filter in chain
+      filterChain.doFilter(httpRequest, response);
+
+    } else {
+      httpRequest.setAttribute(filterDoneAttributeName, Boolean.TRUE);
+
+      try {
+        String methodName = httpRequest.getParameter(this.hiddenFieldName);
+
+        if ("POST".equalsIgnoreCase(httpRequest.getMethod()) && !Strings.empty(methodName)) {
+          String methodNameUppercase = methodName.toUpperCase(Locale.ENGLISH);
+          HttpServletRequest wrapper = new HttpMethodRequestWrapper(methodNameUppercase, httpRequest);
+          filterChain.doFilter(wrapper, response);
+        } else {
+
+          // Filtering done, forward to another filter in chain
+          filterChain.doFilter(httpRequest, response);
+        }
+      } finally {
+        // Remove the filterDone attribute for this request.
+        request.removeAttribute(filterDoneAttributeName);
+      }
+    }
+  }
+
+  public void destroy() {
+  }
+
+
+  private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
+    private final String method;
+
+    public HttpMethodRequestWrapper(String method, HttpServletRequest request) {
+      super(request);
+      this.method = method;
+    }
+
+    @Override
+    public String getMethod() {
+      return this.method;
+    }
+  }
+
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/Localizer.java b/sitebricks/src/main/java/com/google/sitebricks/Localizer.java
new file mode 100644
index 00000000..538059fb
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/Localizer.java
@@ -0,0 +1,262 @@
+package com.google.sitebricks;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.inject.Binder;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+import com.google.sitebricks.compiler.ExpressionCompileException;
+import com.google.sitebricks.compiler.MvelEvaluatorCompiler;
+import com.google.sitebricks.compiler.Parsing;
+import com.google.sitebricks.compiler.Token;
+import com.google.sitebricks.i18n.Message;
+import com.google.sitebricks.rendering.Strings;
+
+/**
+ * A Utility that binds a localizable interface to its instance parameters.
+ */
+class Localizer {
+  private final Binder binder;
+  private final Set localizations;
+
+  // These are the processed, individual message sets by locale.
+  private final Map> localizedValues = Maps.newHashMap();
+
+  // A map to track if we have bound the proxy for a given i18n interface yet.
+  private Set> i18nedSoFar = Sets.newHashSet();
+
+  /**
+   * A value object that represents the localization of an i18n  interface to a locale
+   * and corresponding set of messages. 
+   */
+  static class Localization {
+    // TODO(dhanji): Convert class reference to weak?
+    private final Class clazz;
+    private final Locale locale;
+    private final Map messageBundle;
+
+    public Localization(Class clazz, Locale locale, Map messageBundle) {
+      this.clazz = clazz;
+      this.locale = locale;
+      this.messageBundle = messageBundle;
+    }
+
+  }
+
+  static final Localization DEFAULT = new Localization(null, null, null);
+
+  private Localizer(Binder binder, Set localizations) {
+    this.binder = binder;
+    this.localizations = localizations;
+  }
+
+  public static void localizeAll(Binder binder, Set localizations) {
+    new Localizer(binder, localizations).localize();
+  }
+
+  private void localize() {
+    for (Localization localization : localizations) {
+      // First scan and ensure that all methods on the interface contain i18n params.
+      bindMessages(localization);
+    }
+
+    // We're done with this so we don't need the set anymore.
+    i18nedSoFar = null;
+  }
+
+  private void bindMessages(Localization localization) {
+    Class iface = localization.clazz;
+    Map messages = Maps.newHashMap();
+
+    for (Method method : iface.getMethods()) {
+      Message message = method.getAnnotation(Message.class);
+
+      check(null != message,
+          "Found an i18n interface method missing @Message annotation: ", iface, method);
+
+      if (null != message) {
+        check(!Strings.empty(message.message()),
+            "Empty @Message annotation is not allowed ", iface, method);
+      }
+
+      String template = localization.messageBundle.get(method.getName());
+      check(null != template,
+          "Provided resource bundle does not contain a localization for message: ", iface, method);
+      check(String.class.equals(method.getReturnType()),
+          "All i18n interface methods MUST return String: ", iface, method);
+
+      int argumentCount = method.getParameterTypes().length;
+      Map arguments = Maps.newLinkedHashMap();
+
+      for (int i = 0; i < argumentCount; i++) {
+        Annotation[] annotations = method.getParameterAnnotations()[i];
+
+        check(annotations.length == 1,
+            "Only @Named annotations are allowed on i18n method arguments: ", iface, method);
+        if (annotations.length == 0) {
+          continue;
+        }
+
+        check(Named.class.isInstance(annotations[0]),
+            "Named annotation is missing from i18n interface method argument: ", iface, method);
+
+        // Bind each argument to a template parameter a la Dynamic Finders.
+        arguments.put(((Named) annotations[0]).value(), method.getParameterTypes()[i]);
+      }
+
+      // No point in throwing an NPE ourselves, but we want to keep processing errors so continue
+      if (null == template || null == message) {
+        continue;
+      }
+
+      // Compile arg names against message template to ensure it works.
+      List tokens = null;
+      try {
+        MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(arguments);
+
+        // Compile both the default message as well as the provided localized one.
+        Parsing.tokenize(message.message(), compiler);
+        tokens = Parsing.tokenize(template, compiler);
+      } catch (ExpressionCompileException e) {
+        check(false, "Compile error in i18n message template: \n  " + e.getError().getError() +
+            " in expression " + e.getError().getExpression() +"\n\n  ...in: ", iface, method);
+      }
+
+      // OK now actually go through and build a map between method names and values.
+      messages.put(method.getName(), new MessageDescriptor(tokens, arguments));
+    }
+
+    bindMessageProvider(iface, localization, messages);
+  }
+
+  @SuppressWarnings("unchecked") // We have a guarantee that Proxy will only return subtypes.
+  private void bindMessageProvider(final Class iface,
+                                   Localization localization,
+                                   Map messages) {
+
+    // Add to the value map.
+    localizedValues.put(createLocaleInterfaceKey(iface, localization.locale), messages);
+
+    // Only need to bind the proxy once, for all locales.
+    if (!i18nedSoFar.contains(iface)) {
+      i18nedSoFar.add(iface);
+
+      binder.bind((Class)iface).toProvider(new Provider() {
+
+        // Wonderful Guice hack to get around not using assisted inject.
+        @Inject
+        private final Provider requestProvider = null;
+
+        // This is our delegate field that proxies the interface.
+        private final Object instance = Proxy.newProxyInstance(
+            Thread.currentThread().getContextClassLoader(),
+            new Class[] { iface }, new InvocationHandler() {
+
+              /**
+               * Returns the localized message bundle value, keyed by the method name invoked.
+               */
+              public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+                Locale locale = requestProvider.get().getLocale();
+                Map messages = getMessagesWithFallback(locale);
+
+                // Use default if we don't support the given locale.
+                if (null == messages) {
+                  messages = getMessagesWithFallback(Locale.getDefault());
+                }
+
+                MessageDescriptor descriptor = messages.get(method.getName());
+                if (descriptor == null) {
+                	throw new IllegalStateException("Could not find message '" 
+                			+ method.getName() + "' in " + messages);
+                }
+				return descriptor.render(args);
+              }
+
+			private Map getMessagesWithFallback(Locale locale) {
+				String localeInterfaceKey = createLocaleInterfaceKey(iface, locale);
+				Map result = localizedValues.get(localeInterfaceKey);
+				if (result == null) {
+					result = localizedValues.get(new Locale(locale.getLanguage()));
+				}
+				return result;
+			}
+          });
+
+        // return our proxy here.
+        public Object get() {
+          return instance;
+        }
+
+      });
+    }
+
+  }
+
+  private String createLocaleInterfaceKey(final Class iface, Locale locale) { 
+    return locale.toString() + ":" + iface.getName();
+  }
+
+  private static class MessageDescriptor {
+    private final List tokens;
+    private final Map argumentTypes;
+
+    private MessageDescriptor(List tokens, Map argumentTypes) {
+      this.tokens = tokens;
+      this.argumentTypes = argumentTypes;
+    }
+
+    public String render(Object[] args) {
+      Map arguments = Maps.newHashMap();
+
+      int i = 0;
+      for (String name : argumentTypes.keySet()) {
+        arguments.put(name, args[i]);
+        i++;
+      }
+
+      return Parsing.render(tokens, arguments);
+    }
+
+  }
+
+  private void check(boolean condition, String error, Class key, Method method) {
+    if (!condition) {
+      binder.addError(error + "\n  at " + key.getName() + "." + method.getName() + "()\n");
+    }
+  }
+
+  private static boolean isDefault(Map resourceBundle) {
+    return resourceBundle == DEFAULT;
+  }
+
+  /**
+   * Returns a localization value object describing the defaults specified in the @Message
+   * annotations of the methods on the given i18n interface. The locale used is the system
+   * default.
+   */
+  public static Localization defaultLocalizationFor(Class iface) {
+    Map defaultMessages = Maps.newHashMap();
+
+    for (Method method : iface.getMethods()) {
+      Message msg = method.getAnnotation(Message.class);
+      if (null != msg) {
+        defaultMessages.put(method.getName(), msg.message());
+      }
+    }
+
+    return new Localization(iface, Locale.getDefault(), defaultMessages);
+  }
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/MissingTemplateException.java b/sitebricks/src/main/java/com/google/sitebricks/MissingTemplateException.java
new file mode 100644
index 00000000..c5425fc4
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/MissingTemplateException.java
@@ -0,0 +1,10 @@
+package com.google.sitebricks;
+
+/**
+ * @author Dhanji R. Prasanna (dhanji@gmail com)
+ */
+public class MissingTemplateException extends RuntimeException {
+    public MissingTemplateException(String msg) {
+        super(msg);
+    }
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/MvelEvaluator.java b/sitebricks/src/main/java/com/google/sitebricks/MvelEvaluator.java
new file mode 100644
index 00000000..651102b6
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/MvelEvaluator.java
@@ -0,0 +1,70 @@
+package com.google.sitebricks;
+
+import com.google.common.collect.MapMaker;
+import com.google.sitebricks.compiler.Parsing;
+import net.jcip.annotations.ThreadSafe;
+import org.jetbrains.annotations.Nullable;
+import org.mvel2.CompileException;
+import org.mvel2.MVEL;
+import org.mvel2.PropertyAccessException;
+
+import java.io.Serializable;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * @author Dhanji R. Prasanna (dhanji@gmail.com)
+ */
+@ThreadSafe
+public class MvelEvaluator implements Evaluator {
+
+  //lets do some caching of expressions to see if we cant go a bit faster
+  private final ConcurrentMap compiledExpressions = new MapMaker().makeMap();
+
+  @Nullable
+  public Object evaluate(String expr, Object bean) {
+    Serializable compiled = compiledExpressions.get(expr);
+
+    //compile and store the expr (warms up the expression cache)
+    if (null == compiled) {
+      String preparedExpression = expr;
+
+      //strip expression decorators as necessary
+      if (Parsing.isExpression(expr)) {
+        preparedExpression = Parsing.stripExpression(expr);
+      }
+
+      //compile expression
+      compiled = MVEL.compileExpression(preparedExpression);
+
+      //place into map under original key (i.e. as it came in)
+      compiledExpressions.put(expr, compiled);
+    }
+
+    //lets use mvel to retrieve an expression value instead of a prop
+    try {
+      return MVEL.executeExpression(compiled, bean);
+    } catch (PropertyAccessException e) {
+      throw new IllegalArgumentException(
+          String.format("Could not read property from expression %s (missing a getter?)", expr), e);
+    } catch (NullPointerException npe) {
+      throw new IllegalArgumentException(
+          String.format("Evaluation of property expression [%s] resulted in a NullPointerException",
+              expr), npe);
+    } catch (CompileException e) {
+      throw new IllegalArgumentException(
+          String.format("Compile of property expression [%s] resulted in an error",
+              expr), e);    
+    }
+  }
+
+
+  public void write(String expr, Object bean, Object value) {
+    //lets use mvel to store an expression
+    MVEL.setProperty(bean, expr, value);
+  }
+
+  public Object read(String property, Object contextObject) {
+    return MVEL.getProperty(property, contextObject);
+  }
+
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/NoSuchResourceException.java b/sitebricks/src/main/java/com/google/sitebricks/NoSuchResourceException.java
new file mode 100644
index 00000000..bbc1ccd8
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/NoSuchResourceException.java
@@ -0,0 +1,14 @@
+package com.google.sitebricks;
+
+/**
+ * @author Dhanji R. Prasanna (dhanji@gmail.com)
+ */
+class NoSuchResourceException extends RuntimeException {
+    public NoSuchResourceException(String message) {
+        super(message);
+    }
+
+    public NoSuchResourceException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/PackageScanFailedException.java b/sitebricks/src/main/java/com/google/sitebricks/PackageScanFailedException.java
new file mode 100644
index 00000000..3f81ce1c
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/PackageScanFailedException.java
@@ -0,0 +1,11 @@
+package com.google.sitebricks;
+
+
+/**
+ * @author Dhanji R. Prasanna (dhanji@gmail.com)
+ */
+class PackageScanFailedException extends RuntimeException {
+    public PackageScanFailedException(String s, Exception e) {
+        super(s, e);
+    }
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/PageBinder.java b/sitebricks/src/main/java/com/google/sitebricks/PageBinder.java
new file mode 100644
index 00000000..847ebdf9
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/PageBinder.java
@@ -0,0 +1,61 @@
+package com.google.sitebricks;
+
+import com.google.inject.Key;
+import com.google.inject.binder.ScopedBindingBuilder;
+import com.google.sitebricks.routing.Action;
+
+import java.lang.annotation.Annotation;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ResourceBundle;
+import java.util.regex.Pattern;
+
+/**
+ * @author dhanji@gmail.com (Dhanji R. Prasanna)
+ */
+public interface PageBinder {
+  ShowBinder at(String uri);
+
+  EmbedAsBinder embed(Class clazz);
+
+  void bindMethod(String method, Class annotation);
+
+  NegotiateWithBinder negotiate(String header);
+
+  LocalizationBinder localize(Class iface);
+
+  static interface NegotiateWithBinder {
+    void with(Class ann);
+  }
+
+  static interface ShowBinder extends PerformBinder {
+    ScopedBindingBuilder show(Class clazz);
+    ScopedBindingBuilder serve(Class clazz);
+    void export(String glob);
+  }
+
+  static interface PerformBinder {
+    ActionBinder perform(Action action);
+    ActionBinder perform(Class action);
+    ActionBinder perform(Key action);
+  }
+
+  static interface EmbedAsBinder {
+    ScopedBindingBuilder as(String annotation);
+  }
+
+  static interface LocalizationBinder {
+    void using(Locale locale, Map messages);
+    void using(Locale locale, Properties messages);
+    void using(Locale locale, ResourceBundle messages);
+    void usingDefault();
+  }
+
+  static interface ActionBinder {
+    PerformBinder on(Class... method);
+    ActionBinder select(String param, String value);
+    ActionBinder selectHeader(String param, String value);
+    ActionBinder selectHeader(String param, Pattern regex);
+  }
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/Renderable.java b/sitebricks/src/main/java/com/google/sitebricks/Renderable.java
new file mode 100644
index 00000000..ff42a431
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/Renderable.java
@@ -0,0 +1,18 @@
+package com.google.sitebricks;
+
+import java.util.Set;
+
+/**
+ * @author Dhanji R. Prasanna (dhanji@gmail.com)
+ */
+public interface Renderable {
+    void render(Object bound, Respond respond);
+
+    /**
+     *
+     * @param clazz A class to match.
+     * @return Returns a set of children matching the class, searching down
+     *  to the leaves of the tree. 
+     */
+     Set collect(Class clazz);
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/Respond.java b/sitebricks/src/main/java/com/google/sitebricks/Respond.java
new file mode 100644
index 00000000..02c795fe
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/Respond.java
@@ -0,0 +1,45 @@
+package com.google.sitebricks;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * @author Dhanji R. Prasanna (dhanji@gmail.com)
+ */
+@ImplementedBy(StringBuilderRespond.class)
+public interface Respond {
+  Respond HEADLESS = new StringBuilderRespond();
+
+  void write(String text);
+
+  HtmlTagBuilder withHtml();
+
+  void write(char c);
+
+  void chew();
+
+  String toString();
+
+  void writeToHead(String text);
+
+  void require(String requireString);
+
+  void redirect(String to);
+
+  String getContentType();
+
+  String getRedirect();
+
+  Renderable include(String argument);
+
+  String getHead();
+
+  void clear();
+
+  public static interface HtmlTagBuilder {
+    void textField(String value, String s);
+
+    void headerPlaceholder();
+
+    void textArea(String expression, String s);
+  }
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java b/sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java
new file mode 100644
index 00000000..50a1a41d
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java
@@ -0,0 +1,283 @@
+package com.google.sitebricks;
+
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+import com.google.sitebricks.compiler.Compilers;
+import com.google.sitebricks.compiler.TemplateCompileException;
+import com.google.sitebricks.headless.Service;
+import com.google.sitebricks.rendering.Decorated;
+import com.google.sitebricks.rendering.EmbedAs;
+import com.google.sitebricks.rendering.Templates;
+import com.google.sitebricks.rendering.With;
+import com.google.sitebricks.rendering.control.WidgetRegistry;
+import com.google.sitebricks.rendering.resource.ResourcesService;
+import com.google.sitebricks.routing.PageBook;
+import com.google.sitebricks.routing.PageBook.Page;
+import com.google.sitebricks.routing.SystemMetrics;
+
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static com.google.inject.matcher.Matchers.annotatedWith;
+import static com.google.sitebricks.SitebricksModule.BindingKind.ACTION;
+import static com.google.sitebricks.SitebricksModule.BindingKind.EMBEDDED;
+import static com.google.sitebricks.SitebricksModule.BindingKind.PAGE;
+import static com.google.sitebricks.SitebricksModule.BindingKind.SERVICE;
+import static com.google.sitebricks.SitebricksModule.BindingKind.STATIC_RESOURCE;
+
+/**
+ * @author Dhanji R. Prasanna (dhanji@gmail.com)
+ */
+class ScanAndCompileBootstrapper implements Bootstrapper {
+  private final PageBook pageBook;
+  private final List packages;
+  private final ResourcesService resourcesService;
+  private final WidgetRegistry registry;
+  private final SystemMetrics metrics;
+  private final Compilers compilers;
+
+  @Inject
+  private final Templates templates = null;
+  
+  @Inject @Bricks
+  private final List bindings = null;
+
+  @Inject @Bricks
+  private final Map> methodMap = null;
+
+  @Inject
+  private final Stage currentStage = null;
+
+  @Inject
+  private final Injector injector = null;
+
+  private final Logger log = Logger.getLogger(ScanAndCompileBootstrapper.class.getName());
+
+  @Inject
+  public ScanAndCompileBootstrapper(PageBook pageBook,
+                                    @Bricks List packages,
+                                    ResourcesService resourcesService,
+                                    WidgetRegistry registry,
+                                    SystemMetrics metrics,
+                                    Compilers compilers) {
+
+    this.pageBook = pageBook;
+    this.packages = packages;
+    this.resourcesService = resourcesService;
+    this.registry = registry;
+
+    this.metrics = metrics;
+    this.compilers = compilers;
+  }
+
+  public void start() {
+    Set> set = Sets.newHashSet();
+    for (Package pkg : packages) {
+
+      //look for any classes annotated with @At, @EmbedAs and @With
+      set.addAll(Classes.matching(
+    		  annotatedWith(At.class).or(
+    		  annotatedWith(EmbedAs.class)).or(
+    		  annotatedWith(With.class)).or(
+          annotatedWith(Show.class))
+      ).in(pkg));
+    }
+
+    //we need to scan all the pages first (do not collapse into the next loop)
+    Set pagesToCompile = scanPagesToCompile(set);
+    collectBindings(bindings, pagesToCompile);
+    extendedPages(pagesToCompile);
+
+    // Compile templates for scanned classes (except in dev mode, where faster startup
+    // time is more important and compiles are amortized across visits to each page).
+    // TODO make this configurable separately to stage for GAE
+    if (Stage.DEVELOPMENT != currentStage) {
+      compilePages(pagesToCompile);
+    }
+
+    // Start all services.
+    List> bindings = injector.findBindingsByType(AWARE_TYPE);
+    for (Binding binding : bindings) {
+      binding.getProvider().get().startup();
+    }
+
+    //set application mode to started (now debug mechanics can kick in)
+    metrics.activate();
+  }
+
+  private void extendedPages(Set pagesToCompile) {
+    for (Page page : pagesToCompile) {
+      if (page.pageClass().isAnnotationPresent(Decorated.class)) {
+        // recursively add extension pages
+        analyseExtension(pagesToCompile, page.pageClass());
+      }
+    }
+  }
+
+  //processes all explicit bindings, including static resources.
+  private void collectBindings(List bindings,
+                               Set pagesToCompile) {
+
+    // Reverse the method map for easy lookup of HTTP method annotations.
+    Map, String> methodSet = null;
+
+    //go thru bindings and obtain pages from them.
+    for (SitebricksModule.LinkingBinder binding : bindings) {
+
+      if (EMBEDDED == binding.bindingKind) {
+        if (null == binding.embedAs) {
+          // This can happen if embed() is not followed by an .as(..)
+          throw new IllegalStateException("embed() missing .as() clause: " + binding.pageClass);
+        }
+        registry.addEmbed(binding.embedAs);
+        pagesToCompile.add(pageBook.embedAs(binding.pageClass, binding.embedAs));
+
+      } else if (PAGE == binding.bindingKind) {
+        pagesToCompile.add(pageBook.at(binding.uri, binding.pageClass));
+
+      } else if (STATIC_RESOURCE == binding.bindingKind) {
+        //localize the resource to the SitebricksModule's package.
+        resourcesService.add(SitebricksModule.class, binding.getResource());
+      } else if (SERVICE == binding.bindingKind) {
+        pagesToCompile.add(pageBook.serviceAt(binding.uri, binding.pageClass));
+      } else if (ACTION == binding.bindingKind) {
+        // Lazy create this inverse lookup map, once.
+        if (null == methodSet) {
+          methodSet = HashBiMap.create(methodMap).inverse();
+        }
+        pageBook.at(binding.uri, binding.actionDescriptors, methodSet);
+      }
+    }
+  }
+
+  //goes through the set of scanned classes and builds pages out of them.
+  private Set scanPagesToCompile(Set> set) {
+    Set templates = Sets.newHashSet();
+    Set pagesToCompile = Sets.newHashSet();
+    for (Class pageClass : set) {
+      EmbedAs embedAs = pageClass.getAnnotation(EmbedAs.class);
+      if (null != embedAs) {
+        final String embedName = embedAs.value();
+      
+        //is this a text rendering or embedding-style widget?
+        if (Renderable.class.isAssignableFrom(pageClass)) {
+          @SuppressWarnings("unchecked")
+          Class renderable = (Class) pageClass;
+          registry.add(embedName, renderable);
+        } else {
+          pagesToCompile.add(embed(embedName, pageClass));
+        }
+      }
+      
+      At at = pageClass.getAnnotation(At.class);
+      if (null != at) {
+        if (pageClass.isAnnotationPresent(Service.class)) {
+          pagesToCompile.add(pageBook.serviceAt(at.value(), pageClass));
+        } else if (pageClass.isAnnotationPresent(Export.class)) {
+          //localize the resource to the SitebricksModule's package.
+          resourcesService.add(SitebricksModule.class, pageClass.getAnnotation(Export.class));
+        }
+        else {
+          pagesToCompile.add(pageBook.at(at.value(), pageClass));
+        }
+      }
+
+      if (pageClass.isAnnotationPresent(Show.class)) {
+        // This has a template associated with it.
+        templates.add(new Templates.Descriptor(pageClass,
+            pageClass.getAnnotation(Show.class).value()));
+      }
+    }
+
+    // Eagerly load all detected templates in production mode.
+    if (Stage.DEVELOPMENT != currentStage) {
+      this.templates.loadAll(templates);
+    }
+
+    return pagesToCompile;
+  }
+
+  private void analyseExtension(Set pagesToCompile, Class extendClass) {
+    // store the page with a special page name used by ExtendWidget
+    pagesToCompile.add(pageBook.decorate(extendClass));
+    
+    // recursively analyse super class
+    while (extendClass != Object.class) {
+      extendClass = extendClass.getSuperclass();
+      if (extendClass.isAnnotationPresent(Decorated.class)) {
+        analyseExtension(pagesToCompile, extendClass);
+      }
+      else if (extendClass.isAnnotationPresent(Show.class)) {
+        // there is a @Show with no @Extension so this is the outer template
+        return;
+      }
+    }
+    throw new IllegalStateException("Could not find super class annotated with @Show");
+  }
+
+  private void compilePages(Set pagesToCompile) {
+    final List failures = Lists.newArrayList();
+
+    //perform a compilation pass over all the pages and their templates
+    for (PageBook.Page page : pagesToCompile) {
+      Class pageClass = page.pageClass();
+
+      // Headless web services need to be analyzed but not page-compiled.
+      if (page.isHeadless()) {
+        // TODO(dhanji): Feedback errors as return rather than throwing.
+        compilers.analyze(pageClass);
+        continue;
+      }
+
+      if (log.isLoggable(Level.FINEST)) {
+        log.finest("Compiling template for page " + pageClass.getName());
+      }
+
+      try {
+        compilers.compilePage(page);
+        compilers.analyze(pageClass);
+      } catch (TemplateCompileException e) {
+        failures.add(e);
+      }
+    }
+
+    //log failures if any (we don't abort the app startup)
+    if (!failures.isEmpty()) {
+      logFailures(failures);
+    }
+  }
+
+  private PageBook.Page embed(String embedAs, Class page) {
+    //store custom page wrapped as an embed widget
+    registry.addEmbed(embedAs);
+
+    //store argument name(s) wrapped as an Argument (multiple aliases allowed)
+    if (page.isAnnotationPresent(With.class)) {
+      for (String callWith : page.getAnnotation(With.class).value()) {
+        registry.addArgument(callWith);
+      }
+    }
+
+    //...add as an unbound (to URI) page
+    return pageBook.embedAs(page, embedAs);
+  }
+
+  private void logFailures(List failures) {
+    StringBuilder builder = new StringBuilder();
+    for (TemplateCompileException failure : failures) {
+      builder.append(failure.getMessage());
+      builder.append("\n\n");
+    }
+
+    log.severe(builder.toString());
+  }
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/Show.java b/sitebricks/src/main/java/com/google/sitebricks/Show.java
new file mode 100644
index 00000000..db14ab39
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/Show.java
@@ -0,0 +1,16 @@
+package com.google.sitebricks;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Dhanji R. Prasanna (dhanji@gmail.com)
+ */
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface Show {
+    String value() default "";
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/Shutdowner.java b/sitebricks/src/main/java/com/google/sitebricks/Shutdowner.java
new file mode 100644
index 00000000..3f0373b6
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/Shutdowner.java
@@ -0,0 +1,27 @@
+package com.google.sitebricks;
+
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+import java.util.List;
+
+/**
+ * @author dhanji@gmail.com (Dhanji R. Prasanna)
+ */
+public class Shutdowner {
+  private final Injector injector;
+
+  @Inject
+  public Shutdowner(Injector injector) {
+    this.injector = injector;
+  }
+
+  public void shutdown() {
+    List> bindings = injector.findBindingsByType(Bootstrapper.AWARE_TYPE);
+
+    for (Binding binding : bindings) {
+      injector.getInstance(binding.getKey()).shutdown();
+    }
+  }
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/SitebricksFilter.java b/sitebricks/src/main/java/com/google/sitebricks/SitebricksFilter.java
new file mode 100644
index 00000000..9c480d18
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/SitebricksFilter.java
@@ -0,0 +1,83 @@
+package com.google.sitebricks;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.sitebricks.headless.Reply;
+import com.google.sitebricks.routing.RoutingDispatcher;
+import net.jcip.annotations.Immutable;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author Dhanji R. Prasanna (dhanji@gmail.com)
+ */
+@Immutable
+@Singleton
+class SitebricksFilter implements Filter {
+  private final RoutingDispatcher dispatcher;
+  private final Provider bootstrapper;
+  private final Provider teardowner;
+
+  @Inject
+  public SitebricksFilter(RoutingDispatcher dispatcher, Provider bootstrapper,
+                          Provider teardowner) {
+    this.dispatcher = dispatcher;
+    this.bootstrapper = bootstrapper;
+    this.teardowner = teardowner;
+  }
+
+  public void init(FilterConfig filterConfig) throws ServletException {
+    bootstrapper.get().start();
+  }
+
+  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
+                       FilterChain filterChain)
+      throws IOException, ServletException {
+
+    HttpServletRequest request = (HttpServletRequest) servletRequest;
+    HttpServletResponse response = (HttpServletResponse) servletResponse;
+
+    //dispatch
+    final Respond respond = dispatcher.dispatch(request, response);
+
+    //was there any matching page? (if it was a headless response, we don't need to do anything).
+    // Also we do not do anything if the page elected to do nothing.
+    if (null != respond && null == request.getAttribute(Reply.NO_REPLY_ATTR)) {
+
+      // Only use the string rendering pipeline if this is not a headless request.
+      if (Respond.HEADLESS != respond) {
+      
+        //do we need to redirect or was this a successful render?
+        final String redirect = respond.getRedirect();
+        if (null != redirect) {
+          response.sendRedirect(redirect);
+        } else { //successful render
+
+          // by checking if a content type was set, we allow users to override content-type
+          //  on an arbitrary basis
+          if (null == response.getContentType()) {
+            response.setContentType(respond.getContentType());
+          }
+
+          response.getWriter().write(respond.toString());
+        }
+      }
+    } else {
+      //continue down filter-chain
+      filterChain.doFilter(request, response);
+    }
+  }
+
+  public void destroy() {
+    teardowner.get().shutdown();
+  }
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/SitebricksInternalModule.java b/sitebricks/src/main/java/com/google/sitebricks/SitebricksInternalModule.java
new file mode 100644
index 00000000..19db4efb
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/SitebricksInternalModule.java
@@ -0,0 +1,207 @@
+package com.google.sitebricks;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
+import com.google.inject.Provides;
+import com.google.inject.Stage;
+import com.google.inject.servlet.RequestScoped;
+import com.google.sitebricks.client.Transport;
+import com.google.sitebricks.conversion.MvelConversionHandlers;
+import com.google.sitebricks.headless.Request;
+import com.google.sitebricks.routing.PageBook;
+import com.google.sitebricks.routing.RoutingDispatcher;
+import org.apache.commons.io.IOUtils;
+import org.mvel2.MVEL;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Module encapsulates internal bindings for sitebricks. Can be installed multiple times.
+ */
+class SitebricksInternalModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+
+    //set up MVEL namespace (when jarjar-ed, it will use the repackaged namespace)
+    System.setProperty("mvel.namespace",
+        MVEL.class.getPackage().getName().replace('.', '/') + "/");
+
+    // Bind default content negotiation annotations
+//    install(new ConnegModule()); TODO(dhanji): Fix this--we have to make SitebricksModule multi-installable
+
+    //initialize startup services and routing modules
+    install(PageBook.Routing.module());
+
+    //development mode services
+    if (Stage.DEVELOPMENT.equals(binder().currentStage())) {
+      bind(PageBook.class).to(DebugModePageBook.class);
+      bind(RoutingDispatcher.class).to(DebugModeRoutingDispatcher.class);
+    }
+
+    // use sitebricks converters in mvel
+    requestInjection(new MvelConversionHandlers());
+  }
+
+  @Override
+  public int hashCode() {
+    return SitebricksInternalModule.class.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return SitebricksInternalModule.class.isInstance(obj);
+  }
+
+  @Provides
+  @RequestScoped
+  Request provideRequest(final HttpServletRequest servletRequest, final Injector injector) {
+
+    return new Request() {
+      ImmutableMultimap matrix;
+      ImmutableMultimap headers;
+      ImmutableMultimap params;
+
+      @Override
+      public  RequestRead read(final Class type) {
+        return new RequestRead() {
+          E memo;
+
+          @Override
+          public E as(Class transport) {
+            try {
+              // Only read from the stream once.
+              if (null == memo) {
+                memo = injector.getInstance(transport).in(servletRequest.getInputStream(), type);
+              }
+            } catch (IOException e) {
+              throw new RuntimeException("Unable to obtain input stream from servlet request" +
+                  " (was it already used or closed elsewhere?). Error:\n" + e.getMessage(), e);
+            }
+
+            return memo;
+          }
+        };
+      }
+
+      @Override
+      public void readTo(OutputStream out) throws IOException {
+        IOUtils.copy(servletRequest.getInputStream(), out);
+      }
+
+      @Override
+      public Multimap headers() {
+        if (null == headers) {
+          readHeaders();
+        }
+        return headers;
+      }
+
+      @Override
+      public Multimap params() {
+        if (null == params) {
+          readParams();
+        }
+        return params;
+      }
+
+      @Override
+      public Multimap matrix() {
+        if (null == matrix) {
+          readMatrix();
+        }
+        return matrix;
+      }
+
+      @Override
+      public String matrixParam(String name) {
+        if (null == matrix) {
+          readMatrix();
+        }
+        ImmutableCollection values = matrix.get(name);
+        if (values.size() > 1) {
+          throw new IllegalStateException("This matrix parameter has multiple values, "
+              + name + "=" + values);
+        }
+        return values.isEmpty() ? null : Iterables.getOnlyElement(values);
+      }
+
+      @Override
+      public String param(String name) {
+        return servletRequest.getParameter(name);
+      }
+
+      @Override
+      public String header(String name) {
+        return servletRequest.getHeader(name);
+      }
+
+      private void readParams() {
+        ImmutableMultimap.Builder builder = ImmutableMultimap.builder();
+
+        @SuppressWarnings("unchecked") // Guaranteed by servlet spec
+            Map parameterMap = servletRequest.getParameterMap();
+        for (Map.Entry entry : parameterMap.entrySet()) {
+          builder.putAll(entry.getKey(), entry.getValue());
+        }
+
+        this.params = builder.build();
+      }
+
+      private void readMatrix() {
+        // Do the matrix parameters now.
+        ImmutableMultimap.Builder builder = ImmutableMultimap.builder();
+        String uri = servletRequest.getRequestURI();
+        String[] pieces = uri.split("[/]+");
+        for (String piece : pieces) {
+          String[] pairs = piece.split("[;]+");
+
+          for (String pair : pairs) {
+            String[] singlePair = pair.split("[=]+");
+            if (singlePair.length > 1) {
+              builder.put(singlePair[0], singlePair[1]);
+            }
+          }
+        }
+
+        this.matrix = builder.build();
+      }
+
+      private void readHeaders() {
+        // Build once per request only (so do it here).
+        ImmutableMultimap.Builder builder = ImmutableMultimap.builder();
+
+        @SuppressWarnings("unchecked") // Guaranteed by servlet spec
+            Enumeration headerNames = servletRequest.getHeaderNames();
+        while (headerNames.hasMoreElements()) {
+          String header = headerNames.nextElement();
+
+          @SuppressWarnings("unchecked") // Guaranteed by servlet spec
+              Enumeration values = servletRequest.getHeaders(header);
+          while (values.hasMoreElements()) {
+            builder.put(header, values.nextElement());
+          }
+        }
+
+        this.headers = builder.build();
+      }
+
+    };
+  }
+
+
+  @Provides
+  @RequestScoped
+  Locale provideLocale(HttpServletRequest request) {
+    return request.getLocale();
+  }
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/SitebricksModule.java b/sitebricks/src/main/java/com/google/sitebricks/SitebricksModule.java
new file mode 100644
index 00000000..de16e239
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/SitebricksModule.java
@@ -0,0 +1,322 @@
+package com.google.sitebricks;
+
+import java.lang.annotation.Annotation;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ResourceBundle;
+import java.util.Set;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.inject.AbstractModule;
+import com.google.inject.Key;
+import com.google.inject.Scope;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.ScopedBindingBuilder;
+import com.google.inject.multibindings.Multibinder;
+import com.google.sitebricks.compiler.Parsing;
+import com.google.sitebricks.conversion.Converter;
+import com.google.sitebricks.conversion.ConverterUtils;
+import com.google.sitebricks.core.CaseWidget;
+import com.google.sitebricks.headless.Service;
+import com.google.sitebricks.http.Delete;
+import com.google.sitebricks.http.Get;
+import com.google.sitebricks.http.Head;
+import com.google.sitebricks.http.Post;
+import com.google.sitebricks.http.Put;
+import com.google.sitebricks.http.Trace;
+import com.google.sitebricks.http.negotiate.Accept;
+import com.google.sitebricks.http.negotiate.Negotiation;
+import com.google.sitebricks.rendering.Strings;
+import com.google.sitebricks.routing.Action;
+
+/**
+ * @author dhanji@gmail.com (Dhanji R. Prasanna)
+ */
+public class SitebricksModule extends AbstractModule implements PageBinder {
+
+  // Configure defaults via this contructor.
+  public SitebricksModule() {
+    // By default these are the method annotations we dispatch against.
+    // They can be overridden with custom annotation types.
+    methods.put("get", Get.class);
+    methods.put("post", Post.class);
+    methods.put("put", Put.class);
+    methods.put("delete", Delete.class);
+    methods.put("head", Head.class);
+    methods.put("trace", Trace.class);
+    
+  }
+  
+  @Override
+  protected final void configure() {
+
+    // Re-route all requests through sitebricks.
+    install(servletModule());
+    install(new SitebricksInternalModule());
+
+    // negotiations stuff (make sure we clean this up).
+    negotiate("Accept").with(Accept.class);
+
+    //TODO: yes this is not so nice, but will keep on trying to localize the converter code. jvz.
+    converters = Multibinder.newSetBinder(binder(), Converter.class);
+
+    // TODO remove when more of sitebricks internals is guiced
+    requestStaticInjection(Parsing.class);
+
+    // Call down to the implementation.
+    configureSitebricks();
+
+    // These need to be registered after configureSitebricks because contributions made to the Multibinder
+    // must be allowed to register before all the defaults are registered. In the acceptance tests where the
+    // date format is non-default it tests will fail if the Multibinder is created and the default converters
+    // registered immediately afterward. jvz.
+    /* converters = */ConverterUtils.createConverterMultibinder(converters);
+
+    //insert core widgets set
+    packages.add(0, CaseWidget.class.getPackage());
+
+    bind(new TypeLiteral>() {})
+        .annotatedWith(Bricks.class)
+        .toInstance(packages);
+
+    bind(new TypeLiteral>() {})
+        .annotatedWith(Bricks.class)
+        .toInstance(bindings);
+
+    // These are the HTTP methods that we listen for.
+    bind(new TypeLiteral>>() {})
+        .annotatedWith(Bricks.class)
+        .toInstance(methods);
+
+    // These are Content negotiation annotations.
+    bind(new TypeLiteral>>() {})
+        .annotatedWith(Negotiation.class)
+        .toInstance(negotiations);
+
+    Localizer.localizeAll(binder(), localizations);
+  }
+  
+  /**
+   * Optionally supply {@link javax.servlet.Servlet} and/or {@link javax.servlet.Filter} implementations to
+   * Guice Servlet. See {@link com.google.sitebricks.SitebricksServletModule} for usage examples.
+   *
+   * @see com.google.sitebricks.SitebricksServletModule
+   *
+   * @return An instance of {@link com.google.sitebricks.SitebricksServletModule}. Implementing classes
+   * must return a non-null value.
+   */
+  protected SitebricksServletModule servletModule() {
+    return new SitebricksServletModule();
+  }
+
+  protected void configureSitebricks() {
+  }
+
+  // Bindings.
+  private final List bindings = Lists.newArrayList();
+  private final List packages = Lists.newArrayList();
+  private final Map> methods = Maps.newHashMap();
+  private final Map> negotiations = Maps.newHashMap();
+  private final Set localizations = Sets.newHashSet();
+
+  public final ShowBinder at(String uri) {
+    LinkingBinder binding = new LinkingBinder(uri);
+    bindings.add(binding);
+    return binding;
+  }
+
+  public final EmbedAsBinder embed(Class clazz) {
+    LinkingBinder binding = new LinkingBinder(clazz);
+    bindings.add(binding);
+    return binding;
+  }
+
+  public final void bindMethod(String method, Class annotation) {
+    Strings.nonEmpty(method, "The REST method must be a valid non-empty string");
+    Preconditions.checkArgument(null != annotation);
+
+    String methodNormal = method.toLowerCase();
+    methods.put(methodNormal, annotation);
+  }
+
+  public NegotiateWithBinder negotiate(final String header) {
+    Strings.nonEmpty(header, "invalid request header string for negotiation.");
+    return new NegotiateWithBinder() {
+      public void with(Class ann) {
+        Preconditions.checkArgument(null != ann);
+        negotiations.put(header, ann);
+      }
+    };
+  }
+
+  public LocalizationBinder localize(final Class iface) {
+    Preconditions.checkArgument(iface.isInterface(), "localize() accepts an interface type only");
+    localizations.add(Localizer.defaultLocalizationFor(iface));
+    return new LocalizationBinder() {
+      public void using(Locale locale, Map messages) {
+        localizations.add( new Localizer.Localization(iface, locale, messages));
+      }
+
+      public void using(Locale locale, Properties properties) {
+        Preconditions.checkArgument(null != properties, "Must provide a non-null resource bundle");
+        // A Properties object is always of type string/string
+        @SuppressWarnings({ "unchecked", "rawtypes" }) 
+        Map messages = (Map) properties;
+        localizations.add(new Localizer.Localization(iface, locale, messages));
+      }
+
+      public void using(Locale locale, ResourceBundle bundle) {
+        Preconditions.checkArgument(null != bundle, "Must provide a non-null resource bundle");
+        Map messages = Maps.newHashMap();
+
+        Enumeration keys = bundle.getKeys();
+        while (keys.hasMoreElements()) {
+          String key = keys.nextElement();
+          messages.put(key, bundle.getString(key));
+        }
+        localizations.add(new Localizer.Localization(iface, locale, messages));
+      }
+
+      public void usingDefault() {
+        localizations.add(Localizer.defaultLocalizationFor(iface));
+      }
+    };
+  }
+
+  protected final void scan(Package pack) {
+    Preconditions.checkArgument(null != pack, "Package parameter to scan() cannot be null");
+    packages.add(pack);
+  }
+  
+  static enum BindingKind {
+    EMBEDDED, PAGE, SERVICE, STATIC_RESOURCE, ACTION
+  }
+
+  class LinkingBinder implements ShowBinder, ScopedBindingBuilder, EmbedAsBinder {
+    BindingKind bindingKind;
+    String embedAs;
+    final String uri;
+    Class pageClass;
+    private String resource;
+    private boolean asEagerSingleton;
+    private Class scopeAnnotation;
+    private Scope scope;
+    List actionDescriptors = Lists.newArrayList();
+
+
+    public LinkingBinder(String uri) {
+      this.uri = uri;
+      this.pageClass = null;
+      this.bindingKind = BindingKind.PAGE;
+    }
+
+    public LinkingBinder(Class pageClass) {
+      this.pageClass = pageClass;
+      this.uri = null;
+      this.bindingKind = BindingKind.EMBEDDED;
+    }
+
+    Export getResource() {
+      return new Export() {
+        public String at() {
+          return uri;
+        }
+
+        public String resource() {
+          return resource;
+        }
+
+        public Class annotationType() {
+          return Export.class;
+        }
+      };
+    }
+
+    public ScopedBindingBuilder show(Class clazz) {
+      Preconditions.checkArgument(!clazz.isAnnotationPresent(Service.class),
+          "Cannot show() a headless web service. Did you mean to call serve() instead?");
+      this.pageClass = clazz;
+      this.bindingKind = BindingKind.PAGE;
+
+      return this;
+    }
+
+    public ScopedBindingBuilder serve(Class clazz) {
+      this.pageClass = clazz;
+      this.bindingKind = BindingKind.SERVICE;
+
+      return this;
+    }
+
+    public void export(String glob) {
+      resource = glob;
+      this.bindingKind = BindingKind.STATIC_RESOURCE;
+    }
+
+    public ActionBinder perform(Action action) {
+      this.bindingKind = BindingKind.ACTION;
+      ActionDescriptor ad = new ActionDescriptor(action, this);
+      actionDescriptors.add(ad);
+      return ad;
+    }
+
+    public ActionBinder perform(Class action) {
+      this.bindingKind = BindingKind.ACTION;
+      ActionDescriptor ad = new ActionDescriptor(Key.get(action), this);
+      actionDescriptors.add(ad);
+      return ad;
+    }
+
+    public ActionBinder perform(Key action) {
+      this.bindingKind = BindingKind.ACTION;
+      ActionDescriptor ad = new ActionDescriptor(action, this);
+      actionDescriptors.add(ad);
+      return ad;
+    }
+
+    public ScopedBindingBuilder as(String annotation) {
+      this.embedAs = annotation;
+      return this;
+    }
+
+    public void in(Class scopeAnnotation) {
+      Preconditions.checkArgument(null == scope);
+      Preconditions.checkArgument(!asEagerSingleton);
+      this.scopeAnnotation = scopeAnnotation;
+    }
+
+    public void in(Scope scope) {
+      Preconditions.checkArgument(null == scopeAnnotation);
+      Preconditions.checkArgument(!asEagerSingleton);
+      this.scope = scope;
+    }
+
+    public void asEagerSingleton() {
+      Preconditions.checkArgument(null == scopeAnnotation);
+      Preconditions.checkArgument(null == scope);
+      this.asEagerSingleton = true;
+    }
+  }
+  
+  //
+  // Converters
+  //
+  
+  @SuppressWarnings("rawtypes")
+  private Multibinder converters;
+  
+  public final void converter(Converter converter)    {
+    Preconditions.checkArgument(null != converter, "Type converters cannot be null");
+    converters.addBinding().toInstance(converter);
+  }
+  
+  public final void converter(Class> clazz) {
+    converters.addBinding().to(clazz);
+  }  
+}
diff --git a/sitebricks/src/main/java/com/google/sitebricks/SitebricksServletModule.java b/sitebricks/src/main/java/com/google/sitebricks/SitebricksServletModule.java
new file mode 100644
index 00000000..572c8739
--- /dev/null
+++ b/sitebricks/src/main/java/com/google/sitebricks/SitebricksServletModule.java
@@ -0,0 +1,87 @@
+package com.google.sitebricks;
+
+import com.google.inject.servlet.ServletModule;
+
+/**
+ * Provides an optional mechanism for users of Sitebricks to supply {@link javax.servlet.Servlet} and
+ * {@link javax.servlet.Filter} implementations using the standard Guice Servlet APIs.
+ *
+ * For example: + *
+  public Injector getInjector() {
+    return Guice.createInjector(new SitebricksModule() {
+
+      @Override
+      protected SitebricksServletModule servletModule() {
+        return new SitebricksServletModule() {
+
+          @Override
+          protected void configurePreFilters() {
+            filter("/*").through(MyPreFilter.class);
+          }
+
+          @Override
+          protected void configurePreFilters() {
+            filter("/*").through(MyPostFilter.class);
+          }
+
+          @Override
+          protected void configureCustomServlets() {
+            serve("/foo").with(FooServlet.class);
+          }
+        };
+      }
+
+
+      @Override
+      protected void configureSitebricks() {
+        ...
+      }
+    }
+ }
+
+ */ +public class SitebricksServletModule extends ServletModule { + + @Override + protected final void configureServlets() { + configurePreFilters(); + + filter("/*").through(HiddenMethodFilter.class); + filter("/*").through(SitebricksFilter.class); + + configurePostFilters(); + configureCustomServlets(); + } + + /** + * Provides a mechanism for users of Sitebricks to register their own {@link javax.servlet.Servlet} implementations + * with Guice Servlet via {@link ServletModule#serve(String, String...) serve} and + * {@link ServletModule#serveRegex(String, String...) serveRegex}.

+ */ + protected void configureCustomServlets() { + } + + + /** + * Provides a mechanism for users of Sitebricks to register their own {@link javax.servlet.Filter} implementation with + * Guice Servlet via {@link ServletModule#filter(String, String...) filter} and + * {@link ServletModule#filterRegex(String, String...) filterRegex}.

+ *
+ * Filters declared in this method will execute in the filter chain before the Sitebricks filter invokes. + */ + protected void configurePreFilters() { + } + + /** + * Provides a mechanism for users of Sitebricks to register their own {@link javax.servlet.Filter} implementation with + * Guice Servlet via {@link ServletModule#filter(String, String...) filter} and + * {@link ServletModule#filterRegex(String, String...) filterRegex}.

+ *
+ * Filters declared in this method will execute in the filter chain only if Sitebricks determines it will not + * handle the request. + */ + protected void configurePostFilters() { + } + +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/StringBuilderRespond.java b/sitebricks/src/main/java/com/google/sitebricks/StringBuilderRespond.java new file mode 100644 index 00000000..2b4ad821 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/StringBuilderRespond.java @@ -0,0 +1,139 @@ +package com.google.sitebricks; + +import net.jcip.annotations.NotThreadSafe; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@NotThreadSafe +public class StringBuilderRespond implements Respond { + + private static final String TEXT_TAG_TEMPLATE = "sitebricks.template.textfield"; + private static final String TEXTAREA_TAG_TEMPLATE = "sitebricks.template.textarea"; + + //TODO Improve performance by using an insertion index rather than a placeholder string + private static final String HEADER_PLACEHOLDER = "__sb:PLACEhOlDeR:__"; + + private static final AtomicReference> templates = + new AtomicReference>(); + + private static final String TEXT_HTML = "text/html"; + + @SuppressWarnings("unchecked") + public StringBuilderRespond() { + if (null == templates.get()) { + final Properties properties = new Properties(); + try { + properties.load(StringBuilderRespond.class.getResourceAsStream("templates.properties")); + } catch (IOException e) { + throw new NoSuchResourceException("Can't find templates.properties", e); + } + + //Concurrent/idempotent + templates.compareAndSet(null, (Map) properties); + } + } + + private final StringBuilder out = new StringBuilder(); + private final StringBuilder head = new StringBuilder(); + + //TODO use SortedSet for clustering certain tag types together. + private final Set requires = new LinkedHashSet(); + private String redirect; + + public String getHead() { + return head.toString(); + } + + public void write(String text) { + out.append(text); + } + + public HtmlTagBuilder withHtml() { + return new HtmlBuilder(); + } + + public void write(char c) { + out.append(c); + } + + public void require(String require) { + requires.add(require); + } + + public void redirect(String to) { + this.redirect = to; + } + + public void writeToHead(String text) { + head.append(text); + } + + public void chew() { + out.deleteCharAt(out.length() - 1); + } + + public String getRedirect() { + return redirect; + } + + public Renderable include(String argument) { + return null; + } + + public String getContentType() { + return TEXT_HTML; + } + + public void clear() { + if (null != out) { + out.delete(0, out.length()); + } + if (null != head) { + head.delete(0, head.length()); + } + } + + @Override + public String toString() { + //write requires to header first... + for (String require : requires) { + writeToHead(require); + } + + //write header to placeholder... + //TODO optimize by scanning upto only (if no head) + int index = out.indexOf(HEADER_PLACEHOLDER); + + String output = out.toString(); + + if (index > 0) { + output = output.replaceFirst(HEADER_PLACEHOLDER, head.toString()); + } + + return output; + } + + //do NOT make this a static inner class! + private class HtmlBuilder implements HtmlTagBuilder { + + public void textField(String bind, String value) { + write(String.format(templates.get().get(TEXT_TAG_TEMPLATE), bind, value)); + } + + public void headerPlaceholder() { + write(HEADER_PLACEHOLDER); + } + + public void textArea(String bind, String value) { + write(String.format(templates.get().get(TEXTAREA_TAG_TEMPLATE), bind, value)); + } + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/Template.java b/sitebricks/src/main/java/com/google/sitebricks/Template.java new file mode 100644 index 00000000..27bf3014 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/Template.java @@ -0,0 +1,44 @@ +package com.google.sitebricks; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class Template { + private final Kind templateKind; + private final String text; + + public Template(Kind templateKind, String text) { + this.templateKind = templateKind; + this.text = text; + } + + public Kind getKind() { + return templateKind; + } + + public String getText() { + return text; + } + + public static enum Kind { + HTML, XML, FLAT, MVEL, FREEMARKER; + + /** + * Returns whether a given template should be treated as html, xml or flat + * (currently by looking at file extension) + */ + public static Kind kindOf(String template) { + if (template.endsWith(".html") || template.endsWith(".xhtml")) + return HTML; + else if (template.endsWith(".xml")) + return XML; + else if (template.endsWith(".mvel")) + return MVEL; + else if (template.endsWith(".fml")) + return FREEMARKER; + else + return FLAT; + } + + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/TemplateLoader.java b/sitebricks/src/main/java/com/google/sitebricks/TemplateLoader.java new file mode 100644 index 00000000..2566d2d1 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/TemplateLoader.java @@ -0,0 +1,176 @@ +package com.google.sitebricks; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import net.jcip.annotations.Immutable; + +import javax.servlet.ServletContext; +import java.io.*; +import java.net.URL; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Immutable +public class TemplateLoader { + private final Provider context; + + private final String[] fileNameTemplates = new String[] { "%s.html", "%s.xhtml", "%s.xml", + "%s.txt", "%s.fml", "%s.mvel" }; + + @Inject + public TemplateLoader(Provider context) { + this.context = context; + } + + public Template load(Class pageClass) { + // try to find the template name + Show show = pageClass.getAnnotation(Show.class); + String template = null; + if (null != show) { + template = show.value(); + } + + // an empty string means no template name was given + if (template == null || template.length() == 0) { + // use the default name for the page class + template = resolve(pageClass); + } + + String text; + try { + InputStream stream = null; + //first look in class neighborhood for template + if (null != template) { + stream = pageClass.getResourceAsStream(template); + } + + //look on the webapp resource path if not in classpath + if (null == stream) { + + final ServletContext servletContext = context.get(); + if (null != template) + stream = open(template, servletContext); + + //resolve again, but this time on the webapp resource path + if (null == stream) { + final ResolvedTemplate resolvedTemplate = resolve(pageClass, servletContext, template); + if (null != resolvedTemplate) { + template = resolvedTemplate.templateName; + stream = resolvedTemplate.resource; + } + } + + //if there's still no template, then error out + if (null == stream) { + throw new MissingTemplateException(String.format("Could not find a suitable template for %s, " + + "did you remember to place an @Show? None of [" + + fileNameTemplates[0] + + "] could be found in either package [%s], in the root of the resource dir OR in WEB-INF/.", + pageClass.getName(), pageClass.getSimpleName(), + pageClass.getPackage().getName())); + } + } + + text = read(stream); + } catch (IOException e) { + throw new TemplateLoadingException("Could not load template for (i/o error): " + pageClass, e); + } + + return new Template(Template.Kind.kindOf(template), text); + } + + private ResolvedTemplate resolve(Class pageClass, ServletContext context, String template) { + //first resolve using url conversion + for (String nameTemplate : fileNameTemplates) { + String templateName = String.format(nameTemplate, pageClass.getSimpleName()); + InputStream resource = open(templateName, context); + + if (null != resource) { + return new ResolvedTemplate(templateName, resource); + } + + resource = openWebInf(templateName, context); + + if (null != resource) { + return new ResolvedTemplate(templateName, resource); + } + + + if (null == template) { + continue; + } + //try to resolve @Show template from web-inf folder + resource = openWebInf(template, context); + + if (null != resource) { + return new ResolvedTemplate(template, resource); + } + } + + //resolve again using servlet context if that fails + for (String nameTemplate : fileNameTemplates) { + String templateName = String.format(nameTemplate, pageClass.getSimpleName()); + InputStream resource = context.getResourceAsStream(templateName); + + if (null != resource) { + return new ResolvedTemplate(templateName, resource); + } + } + + return null; + } + + private static class ResolvedTemplate { + private final InputStream resource; + private final String templateName; + + private ResolvedTemplate(String templateName, InputStream resource) { + this.templateName = templateName; + this.resource = resource; + } + } + + private static InputStream open(String file, ServletContext context) { + try { + String path = context.getRealPath(file); + return path == null ? null : new FileInputStream(new File(path)); + } catch (FileNotFoundException e) { + return null; + } + } + + private static InputStream openWebInf(String file, ServletContext context) { + return open("/WEB-INF/" + file, context); + } + + //resolves a location for this page class's template (assuming @Show is not present) + private String resolve(Class pageClass) { + for (String nameTemplate : fileNameTemplates) { + String name = String.format(nameTemplate, pageClass.getSimpleName()); + URL resource = pageClass.getResource(name); + + if (null != resource) { + return name; + } + } + + return null; + } + + private static String read(InputStream stream) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8")); + + StringBuilder builder = new StringBuilder(); + try { + while (reader.ready()) { + builder.append(reader.readLine()); + builder.append("\n"); + } + } finally { + stream.close(); + } + + return builder.toString(); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/TemplateLoadingException.java b/sitebricks/src/main/java/com/google/sitebricks/TemplateLoadingException.java new file mode 100644 index 00000000..4bf82156 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/TemplateLoadingException.java @@ -0,0 +1,14 @@ +package com.google.sitebricks; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +class TemplateLoadingException extends RuntimeException { + public TemplateLoadingException(String msg, Exception e) { + super(msg, e); + } + + public TemplateLoadingException(String msg) { + super(msg); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/Visible.java b/sitebricks/src/main/java/com/google/sitebricks/Visible.java new file mode 100644 index 00000000..b39af8ac --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/Visible.java @@ -0,0 +1,24 @@ +// Copyright 2009. Google, Inc. All Rights Reserved. +package com.google.sitebricks; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +/** + * Annotate a field with this marker to denote it + * visible in templates. By defauly a property is + * read/write. In other words, request parameters + * matching the field name will be bound to the + * field. Setting {@code readOnly} to true will + * prevent this from happening and only expose the + * field to be read into templates. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Visible { + boolean readOnly() default false; +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/binding/ConcurrentPropertyCache.java b/sitebricks/src/main/java/com/google/sitebricks/binding/ConcurrentPropertyCache.java new file mode 100644 index 00000000..2ec571ca --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/binding/ConcurrentPropertyCache.java @@ -0,0 +1,44 @@ +package com.google.sitebricks.binding; + +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +class ConcurrentPropertyCache implements PropertyCache { + private final ConcurrentMap, Map> cache = + new ConcurrentHashMap, Map>(); + + public boolean exists(String property, Class anObjectClass) { + + Map properties = cache.get(anObjectClass); + + //cache bean properties if needed + if (null == properties) { + PropertyDescriptor[] propertyDescriptors; + try { + propertyDescriptors = Introspector + .getBeanInfo(anObjectClass) + .getPropertyDescriptors(); + + } catch (IntrospectionException e) { + throw new IllegalArgumentException(); + } + + properties = new LinkedHashMap(); + for (PropertyDescriptor descriptor : propertyDescriptors) { + properties.put(descriptor.getName(), descriptor.getName()); //apply labels here as needed + } + + cache.putIfAbsent(anObjectClass, properties); + } + + return properties.containsKey(property); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/binding/CookieBasedFlashCache.java b/sitebricks/src/main/java/com/google/sitebricks/binding/CookieBasedFlashCache.java new file mode 100644 index 00000000..01d2135e --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/binding/CookieBasedFlashCache.java @@ -0,0 +1,97 @@ +package com.google.sitebricks.binding; + +import com.google.common.collect.MapMaker; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import net.jcip.annotations.ThreadSafe; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.Serializable; +import java.util.UUID; +import java.util.concurrent.ConcurrentMap; + +/** + * Used to store binding (or forwarding) information between successive requests. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ThreadSafe @Singleton +class CookieBasedFlashCache implements FlashCache, Serializable { + private final ConcurrentMap cache = new MapMaker() + .concurrencyLevel(64) + .makeMap(); + + /** + * Name of the cookie we use to create flash-scoping. I.e. consecutive + * request detection (without sessions). + */ + private static final String FLASH_COOKIE = "X-SB-Flash"; + private final Provider request; + private final Provider response; + + @Inject + public CookieBasedFlashCache(Provider request, + Provider response) { + this.request = request; + this.response = response; + } + + @SuppressWarnings("unchecked") + public T get(String key) { + String cookieId = findCookie(); + if (cookieId == null) { + return null; + } + + // This is how the cookie is constructed in {@linkplain #put} + key = cookieId + key; + + return (T) cache.get(key); + } + + @SuppressWarnings("unchecked") + public T remove(String key) { + String cookieId = findCookie(); + if (cookieId == null) { + return null; + } + + // This is how the cookie is constructed in {@linkplain #put} + key = cookieId + key; + return (T) cache.remove(key); + } + + public void put(String key, T t) { + + String cookieId = (String) request.get().getAttribute(FLASH_COOKIE); + if (null == cookieId) { + // seed a cookie for the next time this user comes back + cookieId = UUID.randomUUID().toString(); + response.get().addCookie(new Cookie(FLASH_COOKIE, cookieId)); + + // memo for this request... (we only need to set the cookie once per request) + request.get().setAttribute(FLASH_COOKIE, cookieId); + } + + // Compose a key from the cookied id + the store key + // We use the cookied id first coz the first 5 chars of + // String are used for generating a hash. + key = cookieId + key; + + cache.put(key, t); + } + + private String findCookie() { + String cookieId = null; + for (Cookie cookie : request.get().getCookies()) { + if (FLASH_COOKIE.equals(cookie.getName())) { + cookieId = cookie.getValue(); + } + } + + return cookieId; + } +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/binding/FlashCache.java b/sitebricks/src/main/java/com/google/sitebricks/binding/FlashCache.java new file mode 100644 index 00000000..84e3cedb --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/binding/FlashCache.java @@ -0,0 +1,18 @@ +package com.google.sitebricks.binding; + +import com.google.inject.ImplementedBy; +import org.jetbrains.annotations.Nullable; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ImplementedBy(NoFlashCache.class) +public interface FlashCache { + @Nullable + T get(String key); + + @Nullable + T remove(String key); + + void put(String key, T t); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/binding/GaeFlashCache.java b/sitebricks/src/main/java/com/google/sitebricks/binding/GaeFlashCache.java new file mode 100644 index 00000000..5d8c7713 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/binding/GaeFlashCache.java @@ -0,0 +1,42 @@ +package com.google.sitebricks.binding; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import net.jcip.annotations.ThreadSafe; + +import javax.servlet.http.HttpSession; + +/** + * Used to store binding (or forwarding) information between successive requests. This + * cache is an alternative to the default {@linkplain HttpSessionFlashCache} in that it + * explicitly sets each attribute on the session (which is required by GAE for appstore + * and memcache replication). + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ThreadSafe +public final class GaeFlashCache implements FlashCache { + private final Provider session; + + @Inject + GaeFlashCache(Provider session) { + this.session = session; + } + + @SuppressWarnings("unchecked") + public T get(String key) { + return (T) session.get().getAttribute(key); + } + + public T remove(String key) { + @SuppressWarnings("unchecked") + T previous = (T) get(key); + session.get().removeAttribute(key); + + return previous; + } + + public void put(String key, T t) { + session.get().setAttribute(key, t); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/binding/HttpSessionFlashCache.java b/sitebricks/src/main/java/com/google/sitebricks/binding/HttpSessionFlashCache.java new file mode 100644 index 00000000..fd07ca06 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/binding/HttpSessionFlashCache.java @@ -0,0 +1,32 @@ +package com.google.sitebricks.binding; + +import com.google.common.collect.MapMaker; +import com.google.inject.servlet.SessionScoped; +import net.jcip.annotations.ThreadSafe; + +import java.io.Serializable; +import java.util.concurrent.ConcurrentMap; + +/** + * Used to store binding (or forwarding) information between successive requests. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ThreadSafe @SessionScoped +public class HttpSessionFlashCache implements FlashCache, Serializable { + private final ConcurrentMap cache = new MapMaker().makeMap(); + + @SuppressWarnings("unchecked") + public T get(String key) { + return (T) cache.get(key); + } + + @SuppressWarnings("unchecked") + public T remove(String key) { + return (T) cache.remove(key); + } + + public void put(String key, T t) { + cache.put(key, t); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/binding/InvalidBindingException.java b/sitebricks/src/main/java/com/google/sitebricks/binding/InvalidBindingException.java new file mode 100644 index 00000000..348bd1b1 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/binding/InvalidBindingException.java @@ -0,0 +1,10 @@ +package com.google.sitebricks.binding; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +class InvalidBindingException extends RuntimeException { + public InvalidBindingException(String msg) { + super(msg); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/binding/MvelRequestBinder.java b/sitebricks/src/main/java/com/google/sitebricks/binding/MvelRequestBinder.java new file mode 100644 index 00000000..6568465b --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/binding/MvelRequestBinder.java @@ -0,0 +1,123 @@ +package com.google.sitebricks.binding; + +import com.google.common.collect.Lists; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.rendering.Strings; +import net.jcip.annotations.Immutable; +import org.mvel2.PropertyAccessException; + +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Immutable +@Singleton +class MvelRequestBinder implements RequestBinder { + private final Evaluator evaluator; + private final Provider cacheProvider; + private final Logger log = Logger.getLogger(MvelRequestBinder.class.getName()); + + private static final String VALID_BINDING_REGEX = "[\\w\\.$]*"; + + @Inject + public MvelRequestBinder(Evaluator evaluator, Provider cacheProvider) { + this.evaluator = evaluator; + this.cacheProvider = cacheProvider; + } + + public void bind(HttpServletRequest request, Object o) { + @SuppressWarnings("unchecked") + final Map map = request.getParameterMap(); + + //bind iteratively (last incoming param-value per key, gets bound) + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + + // If there are multiple entry, then this is a collection bind: + final String[] values = entry.getValue(); + + validate(key); + + Object value; + + if (values.length > 1) { + value = Lists.newArrayList(values); + } else { + // If there is only one value, bind as per normal + String rawValue = values[0]; //choose first (and only value) + + //bind from collection? + if (rawValue.startsWith(COLLECTION_BIND_PREFIX)) { + final String[] binding = rawValue.substring(COLLECTION_BIND_PREFIX.length()).split("/"); + if (binding.length != 2) + throw new InvalidBindingException( + "Collection sources must be bound in the form '[C/collection/hashcode'. " + + "Was the request corrupt? Or did you try to bind something manually" + + " with a key starting '[C/'? Was: " + rawValue); + + final Collection collection = cacheProvider.get().get(binding[0]); + + value = search(collection, binding[1]); + } else + value = rawValue; + } + + //apply the bound value to the page object property + try { + evaluator.write(key, o, value); + } catch (PropertyAccessException e) { + + // Do some better error reporting if this is a real exception. + if (e.getCause() instanceof InvocationTargetException) { + addContextAndThrow(o, key, value, e.getCause()); + } + // Log missing property. + if (log.isLoggable(Level.FINE)) { + log.fine(String.format("A property [%s] could not be bound," + + " but not necessarily an error.", key)); + } + } + catch (Exception e) { + addContextAndThrow(o, key, value, e); + } + } + } + + private void addContextAndThrow(Object bound, String key, Object value, Throwable cause) + { + throw new RuntimeException(String.format( + "Problem setting [%s] on instance [%s] with value [%s]", + key, bound, value), cause); + } + + //TODO optimize this to be aggressive based on collection type + //Linear collection search by hashcode + private Object search(Collection collection, String hashKey) { + int hash = Integer.valueOf(hashKey); + + for (Object o : collection) { + if (o.hashCode() == hash) + return o; + } + + //nothing found + return null; + } + + private void validate(String binding) { + //guard against expression-injection attacks + // TODO use an optimized algorithm, rather than a regex? + if (Strings.empty(binding) || !binding.matches(VALID_BINDING_REGEX)) + throw new InvalidBindingException( + "Binding expression (request/form parameter) contained invalid characters: " + binding); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/binding/NoFlashCache.java b/sitebricks/src/main/java/com/google/sitebricks/binding/NoFlashCache.java new file mode 100644 index 00000000..6c3e3c20 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/binding/NoFlashCache.java @@ -0,0 +1,23 @@ +package com.google.sitebricks.binding; + +import com.google.inject.Singleton; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@Singleton +public class NoFlashCache implements FlashCache { + @Override + public T get(String key) { + return null; + } + + @Override + public T remove(String key) { + return null; + } + + @Override + public void put(String key, T t) { + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/binding/PropertyCache.java b/sitebricks/src/main/java/com/google/sitebricks/binding/PropertyCache.java new file mode 100644 index 00000000..64436e0a --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/binding/PropertyCache.java @@ -0,0 +1,11 @@ +package com.google.sitebricks.binding; + +import com.google.inject.ImplementedBy; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ImplementedBy(ConcurrentPropertyCache.class) +public interface PropertyCache { + boolean exists(String property, Class anObjectClass); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/binding/RequestBinder.java b/sitebricks/src/main/java/com/google/sitebricks/binding/RequestBinder.java new file mode 100644 index 00000000..368101ee --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/binding/RequestBinder.java @@ -0,0 +1,15 @@ +package com.google.sitebricks.binding; + +import com.google.inject.ImplementedBy; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ImplementedBy(MvelRequestBinder.class) +public interface RequestBinder { + String COLLECTION_BIND_PREFIX = "[C/"; + + void bind(HttpServletRequest request, Object o); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/AnalysisError.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/AnalysisError.java new file mode 100644 index 00000000..7562339a --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/AnalysisError.java @@ -0,0 +1,104 @@ +package com.google.sitebricks.compiler; + +import org.mvel2.ErrorDetail; + +/** + * Represents a static analysis error or warning due to a + * sitebricks static check failure. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public abstract class AnalysisError { + + public abstract CompileErrors getReason(); + + public static CompileErrorBuilder in(String fragment) { + return new Builder(fragment); + } + + public static interface CompileErrorBuilder { + CompileErrorBuilder near(int line); + + AnalysisError causedBy(ExpressionCompileException e); + + AnalysisError causedBy(CompileErrors reason); + + AnalysisError causedBy(CompileErrors reason, ExpressionCompileException e); + + AnalysisError causedBy(CompileErrors reason, String cause); + } + + private static class Builder implements CompileErrorBuilder { + private final String fragment; + private int line; + + private Builder(String fragment) { + this.fragment = fragment; + } + + public CompileErrorBuilder near(int line) { + this.line = line; + + return this; + } + + public AnalysisError causedBy(ExpressionCompileException e) { + return new AnalysisErrorImpl(fragment, line, e.getError()); + } + + public AnalysisError causedBy(CompileErrors reason) { + return new AnalysisErrorImpl(fragment, line, reason); + } + + public AnalysisError causedBy(CompileErrors reason, ExpressionCompileException e) { + return new AnalysisErrorImpl(fragment, line, e.getError(), reason); + } + + public AnalysisError causedBy(CompileErrors reason, String cause) { + return new AnalysisErrorImpl(fragment, line, + new EvaluatorCompiler.CompileErrorDetail(cause, new ErrorDetail(cause, true)), reason); + } + } + + private static class AnalysisErrorImpl extends AnalysisError { + private final String fragment; + private final int line; + private final EvaluatorCompiler.CompileErrorDetail error; + private final CompileErrors reason; + + public AnalysisErrorImpl(String fragment, int line, EvaluatorCompiler.CompileErrorDetail error) { + this.fragment = fragment; + this.line = line; + this.error = error; + + this.reason = CompileErrors.ILLEGAL_EXPRESSION; + } + + public AnalysisErrorImpl(String fragment, int line, CompileErrors reason) { + this.fragment = fragment; + this.line = line; + this.reason = reason; + + this.error = null; + } + + public AnalysisErrorImpl(String fragment, int line, EvaluatorCompiler.CompileErrorDetail error, + CompileErrors reason) { + + this.fragment = fragment; + this.line = line; + this.error = error; + this.reason = reason; + } + + @Override + public String toString() { + //TODO make this nicer? + return reason.toString(); + } + + public CompileErrors getReason() { + return null; + } + } +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/AnalysisErrors.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/AnalysisErrors.java new file mode 100644 index 00000000..6ad4c804 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/AnalysisErrors.java @@ -0,0 +1,11 @@ +package com.google.sitebricks.compiler; + +/** + * An enumeration of possible static analysis errors when checking + * a sitebricks application. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public enum AnalysisErrors { + +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/AnnotationNode.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/AnnotationNode.java new file mode 100644 index 00000000..4a02a9f0 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/AnnotationNode.java @@ -0,0 +1,62 @@ +package com.google.sitebricks.compiler; + +import org.jsoup.nodes.Node; +import org.jsoup.nodes.TextNode; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.Validate; +import org.apache.commons.lang.StringUtils; + +/** + Based on jsoup.nodes.TextNode by Jonathan Hedley, jonathan@hedley.net + AnnotationNode is for Sitebricks text annotations such as + @Repeat(...) or @ShowIf(true)

+ */ +public class AnnotationNode extends TextNode { + static final String ANNOTATION_KEY = "_annokey"; + static final String ANNOTATION_CONTENT = "_annocontent"; + static final String ANNOTATION = "_annotation"; + + /** + Create a new AnnotationNode representing the supplied (unencoded) text). + + @param annotation raw text + @param baseUri base uri + @see #createFromEncoded(String, String) + */ + public AnnotationNode(String annotation, String baseUri) { + super(annotation, baseUri); + this.annotation(annotation); + } + + public AnnotationNode(String annotation) { + super(annotation, ""); + this.annotation(annotation); + } + + public String nodeName() { + return "#annotation"; + } + + /** + * Set the annotation of this node. + * @param annotation raw annotation + * @return this, for chaining + */ + public AnnotationNode annotation(String annotation) { + this.attr(ANNOTATION, annotation); + String[] kc = AnnotationParser.extractKeyAndContent(annotation); + this.attr(ANNOTATION_KEY, kc[0]); + this.attr(ANNOTATION_CONTENT, kc[1]); + return this; + } + + public Node apply (Node annotate) { + annotate.attr(ANNOTATION, this.attr(ANNOTATION)); + annotate.attr(ANNOTATION_KEY, this.attr(ANNOTATION_KEY)); + annotate.attr(ANNOTATION_CONTENT, this.attr(ANNOTATION_CONTENT)); + + return annotate; + } + +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/AnnotationParser.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/AnnotationParser.java new file mode 100644 index 00000000..f4762a77 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/AnnotationParser.java @@ -0,0 +1,52 @@ +package com.google.sitebricks.compiler; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jsoup.nodes.Node; + + +/** + * @author shawn + */ +public class AnnotationParser { + + // TODO regex is not powerful enough to parse annotation expressions + public static final Pattern WIDGET_ANNOTATION_REGEX = Pattern.compile("(@\\w+(\\([\\w,=\"'/()?:> + * "{@literal @}MyAnn(property = [expr], ...)"
+ * @return A partially parsed array following this structure:
+   *  [0] -> "MyAnn" 
+ * [1] -> "prop = [expr], ..." + *
+ */ + public static String[] extractKeyAndContent(String annotation) { + return Dom.extractKeyAndContent(annotation); // TODO - move it here? + } + + + +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/CompileError.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/CompileError.java new file mode 100644 index 00000000..d6aa702e --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/CompileError.java @@ -0,0 +1,126 @@ +package com.google.sitebricks.compiler; + +import org.mvel2.ErrorDetail; + +/** + * Represents a template compile error or warning (may be due to an MVEL compile failure or a + * sitebricks static check failure. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public abstract class CompileError { + + public abstract String getFragment(); + + public abstract int getLine(); + + public abstract CompileErrors getReason(); + + public abstract EvaluatorCompiler.CompileErrorDetail getCause(); + + public static CompileErrorBuilder in(String fragment) { + return new Builder(fragment); + } + + public static interface CompileErrorBuilder { + CompileErrorBuilder near(int line); + + CompileError causedBy(ExpressionCompileException e); + + CompileError causedBy(CompileErrors reason); + + CompileError causedBy(CompileErrors reason, ExpressionCompileException e); + + CompileError causedBy(CompileErrors reason, String cause); + } + + private static class Builder implements CompileErrorBuilder { + private final String fragment; + private int line; + + private Builder(String fragment) { + this.fragment = fragment; + } + + public CompileErrorBuilder near(int line) { + this.line = line; + + return this; + } + + public CompileError causedBy(ExpressionCompileException e) { + return new CompileErrorImpl(fragment, line, e.getError()); + } + + public CompileError causedBy(CompileErrors reason) { + return new CompileErrorImpl(fragment, line, reason); + } + + public CompileError causedBy(CompileErrors reason, ExpressionCompileException e) { + return new CompileErrorImpl(fragment, line, e.getError(), reason); + } + + public CompileError causedBy(CompileErrors reason, String cause) { + return new CompileErrorImpl(fragment, line, + new EvaluatorCompiler.CompileErrorDetail(cause, new ErrorDetail(cause, true)), reason); + } + } + + private static class CompileErrorImpl extends CompileError { + private final String fragment; + private final int line; + private final EvaluatorCompiler.CompileErrorDetail error; + private final CompileErrors reason; + + public CompileErrorImpl(String fragment, int line, EvaluatorCompiler.CompileErrorDetail error) { + this.fragment = fragment; + this.line = line; + this.error = error; + + this.reason = CompileErrors.ILLEGAL_EXPRESSION; + } + + public CompileErrorImpl(String fragment, int line, CompileErrors reason) { + this.fragment = fragment; + this.line = line; + this.reason = reason; + + this.error = null; + } + + public CompileErrorImpl(String fragment, int line, EvaluatorCompiler.CompileErrorDetail error, + CompileErrors reason) { + + this.fragment = fragment; + this.line = line; + this.error = error; + this.reason = reason; + } + + @Override + public String getFragment() { + return fragment; + } + + @Override + public int getLine() { + return line; + } + + @Override + public CompileErrors getReason() { + return reason; + } + + @Override + public EvaluatorCompiler.CompileErrorDetail getCause() { + return error; + } + + @Override + public String toString() { + //TODO make this nicer? + return reason.toString(); + } + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/CompileErrors.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/CompileErrors.java new file mode 100644 index 00000000..0d7d331f --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/CompileErrors.java @@ -0,0 +1,19 @@ +package com.google.sitebricks.compiler; + +/** + * An enumeration of possible compile errors when checking a template. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public enum CompileErrors { + MISSING_REPEAT_VAR, + MISSING_REPEAT_ITEMS, + REPEAT_OVER_ATOM, + FORM_MISSING_NAME, + UNRESOLVABLE_FORM_BINDING, + UNRESOLVABLE_FORM_ACTION, + MALFORMED_TEMPLATE, + ILLEGAL_EXPRESSION, + PROPERTY_NOT_WRITEABLE, + ERROR_COMPILING_PROPERTY, +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/CompiledToken.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/CompiledToken.java new file mode 100644 index 00000000..17e4bd03 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/CompiledToken.java @@ -0,0 +1,63 @@ +package com.google.sitebricks.compiler; + +import net.jcip.annotations.Immutable; + +import com.google.sitebricks.Evaluator; + +/** + * Created with IntelliJ IDEA. + * On: 20/03/2007 + * + * A simple wrapper around a string or expression (with evaluator), denoting it as a + * renderable token. + * + * @author Dhanji R. Prasanna (dhanji at gmail com) + * @since 1.0 + */ +@Immutable +class CompiledToken implements Token { + private final String token; + private final boolean isExpression; + private final Evaluator evaluator; + + private CompiledToken(String token, boolean expression) { + this.token = token; + this.evaluator = null; + isExpression = expression; + } + + private CompiledToken(Evaluator evaluator, boolean expression) { + this.evaluator = evaluator; + isExpression = expression; + this.token = null; + } + + public boolean isExpression() { + return isExpression; + } + + public String render(Object bound) { + if (isExpression) { + Object object = evaluator.evaluate(null, bound); + if (object instanceof String) { + return (String) object; + } + else { + return Parsing.getTypeConverter().convert(object, String.class); + } + } + else { + return token; + } + } + + //local factories + static CompiledToken expression(String token, EvaluatorCompiler compiler) throws ExpressionCompileException { + //strip leading ${ and trailing } + return new CompiledToken(compiler.compile(token.substring(2, token.length() - 1)), true); + } + + static CompiledToken text(String token) { + return new CompiledToken(token, false); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/Compilers.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/Compilers.java new file mode 100644 index 00000000..055dd6e4 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/Compilers.java @@ -0,0 +1,43 @@ +package com.google.sitebricks.compiler; + +import com.google.inject.ImplementedBy; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.routing.PageBook; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@ImplementedBy(StandardCompilers.class) +public interface Compilers { + Renderable compileHtml(Class page, String template); + + Renderable compileXml(Class page, String template); + + Renderable compileFlat(Class page, String template); + + /** + * Creates a Renderable that can process MVEL templates. + * These are not to be confused with Sitebricks templates + * that *use* MVEL. Rather, this is MVEL's template technology. + */ + Renderable compileMvel(Class page, String template); + + Renderable compileFreemarker( Class page, String text ); + + Renderable compileVelocity(Class page, String template); + + /** + * Performs static analysis of the given page class to + * determine some types of errors. + */ + void analyze(Class page); + + + void compilePage(PageBook.Page page); + + /** + * Convenience method, use this instead of compileXXX to hide + * the underlying template type if you dont care what it is. + */ + Renderable compile(Class templateClass); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/Dom.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/Dom.java new file mode 100644 index 00000000..2d3496f2 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/Dom.java @@ -0,0 +1,190 @@ +package com.google.sitebricks.compiler; + +import net.jcip.annotations.NotThreadSafe; +import org.dom4j.Attribute; +import org.dom4j.Element; +import org.dom4j.Node; +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.XMLFilter; +import org.xml.sax.helpers.AttributesImpl; +import org.xml.sax.helpers.XMLFilterImpl; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * + * Utility class helps XmlTemplateCompiler work with the DOM. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +class Dom { + static final String LINE_NUMBER_ATTRIBUTE = "__WarpWidgetsSaxLineNumber"; + + static final String FORM_TAG = "form"; + static final String XMLNS_ATTRIB_REGEX = " xmlns=\"[a-zA-Z0-9_+%;#/\\-:\\.]*\""; + + private Dom() { + } + + + //is this a form node? + static boolean isForm(Node node) { + return FORM_TAG.equals(node.getName()); + } + + static String stripAnnotation(String text) { + final Matcher matcher = AnnotationParser.WIDGET_ANNOTATION_REGEX + .matcher(text); + + //strip off the ending bit (annotation) + if (matcher.find()) + return text.substring(0, matcher.start()); + + return text; + } + + static String readAnnotation(Node node) { + String annotation = null; + + //if this is a text node, then match for annotations + if (isText(node)) { + final Matcher matcher = AnnotationParser.WIDGET_ANNOTATION_REGEX + .matcher(node.asXML()); + + if (matcher.find()) { + annotation = matcher.group(); + } + } + return annotation; + } + + static String asRawXml(Element element) { + final Element copy = element.createCopy(); + + final Attribute lineNumber = copy.attribute(LINE_NUMBER_ATTRIBUTE); + + if (null != lineNumber) + copy.remove(lineNumber); + + copy.remove(copy.getNamespace()); + + return copy.asXML(); + } + + static boolean skippable(Attribute type) { + if (null == type) + return false; + + final String kind = type.getValue(); + return ( "submit".equals(kind) || "button".equals(kind) || "reset".equals(kind) || "file".equals(kind) ); + } + + /** + * + * @param list A list of dom4j attribs + * @return Returns a mutable map parsed out of the dom4j attribute list + */ + static Map parseAttribs(List list) { + Map attrs = new LinkedHashMap(list.size() + 4); + + for (Object o : list) { + Attribute attribute = (Attribute)o; + + //skip special attributes + if (LINE_NUMBER_ATTRIBUTE.equals(attribute.getName())) + continue; + + attrs.put(attribute.getName(), attribute.getValue()); + } + + return attrs; + } + + /** + * @param annotation A string reprenting an unparsed annotation of the form:
+   * "{@literal @}MyAnn(property = [expr], ...)"
+ * @return A partially parsed array following this structure:
+   *  [0] -> "MyAnn" 
+ * [1] -> "prop = [expr], ..." + *
+ */ + static String[] extractKeyAndContent(String annotation) { + final int index = annotation.indexOf('('); + + //there's no content + if (index < 0) + return new String[] { annotation.substring(1).toLowerCase(), "" }; + + String content = annotation.substring(index + 1, annotation.lastIndexOf(')')); + + //normalize empty string to null + if ("".equals(content)) + content = null; + + return new String[] { annotation.substring(1, index).toLowerCase(), content }; + } + + static boolean isTextCommentOrCdata(Node node) { + final short nodeType = node.getNodeType(); + + return isText(node) || Node.COMMENT_NODE == nodeType || Node.CDATA_SECTION_NODE == nodeType; + } + + static boolean isText(Node node) { + return null != node && Node.TEXT_NODE == node.getNodeType(); + } + + static boolean isElement(Node node) { + return Node.ELEMENT_NODE == node.getNodeType(); + } + + //removes special attributes, so rendering can happen normally + public static void normalizeAttributes(Element element) { + final Attribute toRemove = element.attribute(LINE_NUMBER_ATTRIBUTE); + + if (null != toRemove) + element.remove(toRemove); + } + + /** + * An XML filter used to generate line numbers as a special attribute. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ + @NotThreadSafe + private static class SaxLineNumbersFilter extends XMLFilterImpl { + private Locator locator; + + public void setDocumentLocator(Locator locator) { + this.locator = locator; + + super.setDocumentLocator(locator); + } + + public void startElement(String s, String s1, String s2, Attributes attributes) throws SAXException { + + //replace existing attributes with a decorator that stores line numbers + AttributesImpl attr = new AttributesImpl(attributes); + attr.addAttribute("", "", LINE_NUMBER_ATTRIBUTE, "int", String.valueOf(locator.getLineNumber())); + + super.startElement(s, s1, s2, attr); + } + + } + + public static XMLFilter newLineNumberFilter() { + return new SaxLineNumbersFilter(); + } + + public static int lineNumberOf(Element element) { + final Attribute attribute = element.attribute(LINE_NUMBER_ATTRIBUTE); + + return null == attribute ? -1 : Integer.parseInt(attribute.getValue()); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/EvaluatorCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/EvaluatorCompiler.java new file mode 100644 index 00000000..b09ee5d1 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/EvaluatorCompiler.java @@ -0,0 +1,71 @@ +package com.google.sitebricks.compiler; + +import java.lang.reflect.Type; +import java.util.List; + +import org.mvel2.ErrorDetail; + +import com.google.sitebricks.Evaluator; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public interface EvaluatorCompiler { + + /** + * @param expression An expression to compile against this compiler (and context class) + * + * @return Returns An evaluator that can execute the expression against the context class and a + * real instance of data. + * @throws ExpressionCompileException If there is a problem compiling the expression. + */ + Evaluator compile(String expression) throws ExpressionCompileException; + + + /** + * @param template A String template with embedded EL expressions to convert into a list of + * evaluable tokens. These tokens evaluate against given data or simply render plain text as + * appropriate. Performs a compile of each expression against the context class of this + * EvaluatorCompiler. + * + * @return Returns the egress type of the entire expression chain + * @throws com.google.sitebricks.compiler.ExpressionCompileException If there is a problem + * compiling the expression. + */ + List tokenizeAndCompile(String template) throws ExpressionCompileException; + + /** + * @param expression An expression to compile against this compiler (and context class) + * + * @return Returns the egress type of the entire expression chain + * @throws com.google.sitebricks.compiler.ExpressionCompileException If there is a problem + * compiling the expression. + */ + Type resolveEgressType(String expression) throws ExpressionCompileException; + + /** + * @param property A property of this class to test. + * @return True if the property has a setter or reasonable alternative that can + * be written to by the evaluator produced by this compiler. + */ + boolean isWritable(String property) throws ExpressionCompileException; + + public static class CompileErrorDetail { + private final String expression; + private final ErrorDetail error; + + public CompileErrorDetail(String expression, ErrorDetail error) { + this.expression = expression; + this.error = error; + } + + + public String getExpression() { + return expression; + } + + public ErrorDetail getError() { + return error; + } + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/ExpressionCompileException.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/ExpressionCompileException.java new file mode 100644 index 00000000..3af72932 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/ExpressionCompileException.java @@ -0,0 +1,33 @@ +package com.google.sitebricks.compiler; + +import org.mvel2.ErrorDetail; + +import java.util.List; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public final class ExpressionCompileException extends Throwable { + private final List errors; + private final String expression; + + public ExpressionCompileException(String expression, List errors) { + this.errors = errors; + this.expression = expression; + } + + public ExpressionCompileException(String msg) { + super(msg); + + expression = null; + errors = null; + } + + + public EvaluatorCompiler.CompileErrorDetail getError() { + //TODO is it enough to report just the first error? + //ensure we wrap this in ${} + return new EvaluatorCompiler.CompileErrorDetail(String.format("${%s}", expression), + new ErrorDetail(expression, true)); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/FlatTemplateCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/FlatTemplateCompiler.java new file mode 100644 index 00000000..ad1d28ae --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/FlatTemplateCompiler.java @@ -0,0 +1,50 @@ +package com.google.sitebricks.compiler; + +import com.google.sitebricks.Renderable; +import com.google.sitebricks.rendering.control.WidgetRegistry; +import com.google.sitebricks.routing.SystemMetrics; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Compiles non-XML templates. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + * @see XmlTemplateCompiler + */ +class FlatTemplateCompiler { + private final Class page; + private final MvelEvaluatorCompiler compiler; + private final SystemMetrics metrics; + private final WidgetRegistry registry; + + public FlatTemplateCompiler(Class page, MvelEvaluatorCompiler compiler, + SystemMetrics metrics, WidgetRegistry registry) { + this.page = page; + this.compiler = compiler; + this.metrics = metrics; + this.registry = registry; + } + + public Renderable compile(String template) { + try { + return registry.textWidget(template, compiler); + + } catch (ExpressionCompileException e) { + final List errors = Arrays.asList( + CompileError.in(template) + .near(e.getError().getError().getRow()) + .causedBy(e) + ); + + final List warnings = Collections.emptyList(); + + //log errors and abort compile + metrics.logErrorsAndWarnings(page, errors, warnings); + + throw new TemplateCompileException(page, template, errors, warnings); + } + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/HtmlParser.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/HtmlParser.java new file mode 100644 index 00000000..d2b09ac3 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/HtmlParser.java @@ -0,0 +1,562 @@ +package com.google.sitebricks.compiler; + +import com.google.common.collect.ImmutableSet; +import com.google.sitebricks.rendering.Strings; +import org.apache.commons.lang.Validate; +import org.jsoup.nodes.*; +import org.jsoup.parser.Tag; +import org.jsoup.parser.TokenQueue; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses HTML into a List<{@link org.jsoup.nodes.Node}> + * this is a relaxed version of Jonathan Hedley's {@link org.jsoup.parser.Parser} + */ +public class HtmlParser { + private static final ImmutableSet closingOptional = ImmutableSet + .of("a", "form", "label", "dt", "dd", "li", + "thead", "tfoot", "tbody", "colgroup", "tr", "th", "td"); + + private static final ImmutableSet headTags = ImmutableSet + .of("base", "script", "noscript", "link", "meta", "title", "style", "object"); + + private static final String SQ = "'"; + private static final String DQ = "\""; + + private static final Tag htmlTag = Tag.valueOf("html"); + private static final Tag headTag = Tag.valueOf("head"); + private static final Tag bodyTag = Tag.valueOf("body"); + private static final Tag titleTag = Tag.valueOf("title"); + private static final Tag textareaTag = Tag.valueOf("textarea"); + + // private final ArrayList soup = new ArrayList(); + // private final LinkedList soup = new LinkedList(); + private final LinkedList stack = new LinkedList(); + + // TODO - LineCountingTokenQueue + static final Pattern LINE_SEPARATOR = Pattern.compile("(\\r\\n|\\n|\\r|\\u0085|\\u2028|\\u2029)"); + static final String LINE_NUMBER_ATTRIBUTE = "_linecount"; + + private final TokenQueue tq; + + private String baseUri = ""; + + private Element _html = null; + private Element _head = null; + private Element _body = null; + + private AnnotationNode pendingAnnotation = null; + + private int linecount = 0; + + static final ImmutableSet SKIP_ATTR = ImmutableSet.of(LINE_NUMBER_ATTRIBUTE, + AnnotationNode.ANNOTATION, AnnotationNode.ANNOTATION_KEY, AnnotationNode.ANNOTATION_CONTENT); + + private HtmlParser(String html) { + Validate.notNull(html); + tq = new TokenQueue(html); + } + + /** + * Parse HTML into List + * + * @param html HTML to parse + */ + public static List parse(String html) { + HtmlParser parser = new HtmlParser(html); + return parser.parse(); + } + + /* Parse a fragment of HTML into the {@code body} of a Document. + @param bodyHtml fragment of HTML + @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. + @return Document, with empty head, and HTML parsed into body + */ + // public static Document parseBodyFragment(String bodyHtml, String baseUri) { + // HtmlParser parser = new HtmlParser(bodyHtml, true); + // return parser.parse(); + // } + + private List parse() { + while (!tq.isEmpty()) { + if (tq.matches(" + data = data.substring(0, data.length() - 1); + + Comment comment = new Comment(data, baseUri); + annotate(comment); // TODO - should annotations even apply to comments? + lines(comment, data); + add(comment); + } + + private void parseXmlDecl() { + tq.consume("<"); + Character firstChar = tq.consume(); // "); + + XmlDeclaration decl = new XmlDeclaration(data, baseUri, procInstr); + annotate(decl); // TODO - should annotations even apply to declarations? + lines(decl, data); + add(decl); + + } + + private void parseEndTag() { + tq.consume(""); + + if (!Strings.empty(tagName)) { + Tag tag = Tag.valueOf(tagName); + popStackToClose(tag); + } + } + + private void parseStartTag() { + tq.consume("<"); + String tagName = tq.consumeTagName(); + + if (Strings.empty(tagName)) { // doesn't look like a start tag after all; put < back on stack and handle as text + tq.addFirst("<"); + parseTextNode(); + return; + } + + Attributes attributes = new Attributes(); + while (!tq.matchesAny("<", "/>", ">") && !tq.isEmpty()) { + Attribute attribute = parseAttribute(); + if (attribute != null) + attributes.put(attribute); + } + + Tag tag = Tag.valueOf(tagName); + // TODO - option to create elements without indent + Element child = new Element(tag, baseUri, attributes); + annotate(child); + + lines(child, ""); + + boolean isEmptyElement = tag.isEmpty(); // empty element if empty tag (e.g. img) or self-closed el (
+ if (tq.matchChomp("/>")) { // close empty element or tag + isEmptyElement = true; + } else { + tq.matchChomp(">"); + } + + // pc data only tags (textarea, script): chomp to end tag, add content as text node + if (tag.isData()) { + String data = tq.chompTo(""); + + // enable annotations on data areas + parseAnnotatableText (data, child); + } + + // : update the base uri + if (child.tagName().equals("base")) { + String href = child.absUrl("href"); + if (!Strings.empty(href)) { // ignore etc + baseUri = href; + // TODO - consider updating baseUri for relevant elements in the stack, eg rebase(stack, uri) + // doc.get().setBaseUri(href); // set on the doc so doc.createElement(Tag) will get updated base + } + } + + addChildToParent(child, isEmptyElement); + } + + private Attribute parseAttribute() { + whitespace(); + String key = tq.consumeAttributeKey(); + String value = ""; + whitespace(); + if (tq.matchChomp("=")) { + whitespace(); + + if (tq.matchChomp(SQ)) { + value = tq.chompTo(SQ); + } else if (tq.matchChomp(DQ)) { + value = tq.chompTo(DQ); + } else { + StringBuilder valueAccum = new StringBuilder(); + // no ' or " to look for, so scan to end tag or space (or end of stream) + while (!tq.matchesAny("<", "/>", ">") && !tq.matchesWhitespace() && !tq.isEmpty()) { + valueAccum.append(tq.consume()); + } + value = valueAccum.toString(); + } + whitespace(); + } + if (!Strings.empty(key)) + return Attribute.createFromEncoded(key, value); + else { + tq.consume(); // unknown char, keep popping so not get stuck + return null; + } + } + + + /** + * Pulls a text segment apart by annotations within it and creates multiple Text Nodes + * applying the annotation to each text segment as approriate. + * + * @param text the text to be processed for annotations + * @param parent + */ + private void parseAnnotatableText(String text, Element parent) { + AnnotationNode annotation = null; + Matcher matcher = AnnotationParser.WIDGET_ANNOTATION_REGEX.matcher(text); + + int previousEnd = 0; + while (matcher.find()){ + int start = matcher.start(); + + // build a new text node for what is between last index and current annotation + if (start > previousEnd) { + String segment = text.substring(previousEnd, start); + // ignore empty white space + if (segment.trim().length() > 0){ + addTextNodeToParent (segment, parent, annotation); + annotation = null; + } + } + + // parse the annotation + String annotationText = matcher.group().trim(); + if (null != annotationText) { + annotation = new AnnotationNode(annotationText); + lines(annotation, annotationText); + } + previousEnd = matcher.end(); + } + + // handle leftover text if we parsed some segment + if (previousEnd > 0 && previousEnd < text.length()){ + String segment = text.substring(previousEnd); + if (segment.trim().length() > 0){ + addTextNodeToParent (segment, parent, annotation); + annotation = null; + } + } + + // store the remaining annotation for use by whatever is parsed next + if (annotation != null) + add(annotation); + + // handle no annotations being found + if (previousEnd == 0){ + Node dataNode; + if (parent.tagName().equals(titleTag) || parent.tagName().equals(textareaTag)) + dataNode = TextNode.createFromEncoded(text, baseUri); + else // data not encoded but raw (for " in script) + dataNode = new DataNode(text, baseUri); + lines(dataNode, text); + + if (pendingAnnotation != null) + pendingAnnotation.apply(dataNode); + + // put the text node on the parent + parent.appendChild(dataNode); + } + } + + /** + * Break the text up by the first line delimiter. We only want annotations applied to the first line of a block of text + * and not to a whole segment. + * + * @param text the text to turn into nodes + * @param parent the parent node + * @param annotation the current annotation to be applied to the first line of text + */ + private void addTextNodeToParent (String text, Element parent, AnnotationNode annotation) { + String [] lines = new String[] {text}; + + if (annotation != null) + lines = splitInTwo(text); + + for (int i = 0; i < lines.length; i++){ + TextNode textNode = TextNode.createFromEncoded(lines[i], baseUri); + lines(textNode, lines[i]); + + // apply the annotation and reset it to null + if (annotation != null && i == 0) + annotation.apply(textNode); + + // put the text node on the parent + parent.appendChild(textNode); + } + } + + /** + * Break a text segment apart into two at the first line delimiter which has non-whitespace characters before it. + * + * @param text text to split in two + * @return + */ + private String[] splitInTwo(String text) { + Matcher matcher = LINE_SEPARATOR.matcher(text); + while (matcher.find()){ + int start = matcher.start(); + if (start > 0 && start < text.length()) { + String segment = text.substring(0, start); + if (segment.trim().length() > 0) + return new String[] {text.substring(0, start), text.substring(start)}; + } + } + return new String[] {text}; + } + + private void parseTextNode() { + String text = tq.consumeTo("<"); + String annotationText = AnnotationParser.readAnnotation(text); + text = AnnotationParser.stripAnnotation(text); + + if (text.length() > 0) { + TextNode textNode = TextNode.createFromEncoded(text, baseUri); + // if (pendingAnnotation != null) { pendingAnnotation.apply(textNode); } + lines(textNode, text); + add(textNode); + } + + if (null != annotationText) { + AnnotationNode annotation = new AnnotationNode(annotationText); + lines(annotation, annotationText); + add(annotation); + } + } + + private void parseCdata() { + tq.consume(""); + TextNode textNode = new TextNode(rawText, baseUri); // constructor does not escape + + if (pendingAnnotation != null) + pendingAnnotation.apply(textNode); + + lines(textNode, rawText); + add(textNode); + } + + + private Element addChildToParent(Element child, boolean isEmptyElement) { + Element parent = popStackToSuitableContainer(child.tag()); + if (parent != null) + parent.appendChild(child); + + if (!isEmptyElement && !child.tag().isData()) { + stack.addLast(child); + } + + return parent; + } + + + private boolean stackHasValidParent(Tag childTag) { + if (stack.size() == 1 && childTag.equals(htmlTag)) + return true; // root is valid for html node + + for (int i = stack.size() - 1; i >= 0; i--) { + Node n = stack.get(i); + if (n instanceof Element) + return true; + } + return false; + } + + private Element popStackToSuitableContainer(Tag tag) { + while (!stack.isEmpty() && !(stack.getLast() instanceof XmlDeclaration)) { + Node lastNode = stack.getLast(); + if (lastNode instanceof Element) { + Element last = (Element) lastNode; + if (canContain(last.tag(), tag)) + return last; + else + stack.removeLast(); + } + } + return null; + } + + private Element popStackToClose(Tag tag) { + // first check to see if stack contains this tag; if so pop to there, otherwise ignore + int counter = 0; + Element elToClose = null; + for (int i = stack.size() - 1; i > 0; i--) { + counter++; + Node n = stack.get(i); + if (n instanceof Element) { + Element el = (Element) n; + Tag elTag = el.tag(); + if (elTag.equals(bodyTag) || elTag.equals(headTag) || elTag.equals(htmlTag)) { // once in body, don't close past body + break; + } else if (elTag.equals(tag)) { + elToClose = el; + break; + } + } + } + if (elToClose != null) { + for (int i = 0; i < counter; i++) { + stack.removeLast(); + } + } + return elToClose; + } + + + private void add(N n) { + Node last = null; + + if (stack.size() == 0) { + if (n instanceof XmlDeclaration) { + // only add the first/outermost doctype + stack.add(n); + return; + } + } else { + last = stack.getLast(); + } + + + // TODO - optionally put the AnnotationNode on the stack + if (n instanceof AnnotationNode) { + pendingAnnotation = (AnnotationNode) n; + return; + } +// else if (null != pendingAnnotation) { +// pendingAnnotation.apply(n); +// } + + + if (n instanceof Element) { + Element en = (Element) n; + if (en.tag().equals(htmlTag) && (null == _html)) + _html = en; + + else if (en.tag().equals(htmlTag) && (null != _html)) + for (Node cat : en.childNodes()) _html.appendChild(cat); + + else if (en.tag().equals(headTag) && (null == _head)) + _head = en; + + else if (en.tag().equals(headTag) && (null != _head)) + for (Node cat : en.childNodes()) _head.appendChild(cat); + + else if (en.tag().equals(bodyTag) && (null == _body)) + _body = en; + + else if (en.tag().equals(bodyTag) && (null != _body)) + for (Node cat : en.childNodes()) _body.appendChild(cat); + } + + + if (last == null) + stack.add(n); + + else if (last instanceof Element) { + ((Element) last).appendChild(n); + } + + } + + + // from jsoup.parser.Tag + + /** + * Test if this tag, the prospective parent, can accept the proposed child. + * + * @param child potential child tag. + * @return true if this can contain child. + */ + boolean canContain(Tag parent, Tag child) { + Validate.notNull(child); + + if (child.isBlock() && !parent.canContainBlock()) + return false; + + if (!child.isBlock() && parent.isData()) + return false; + + if (closingOptional.contains(parent.getName()) && parent.getName().equals(child.getName())) + return false; + + if (parent.isEmpty() || parent.isData()) + return false; + + // head can only contain a few. if more than head in here, modify to have a list of valids + // TODO: (could solve this with walk for ancestor) + if (parent.getName().equals("head")) { + if (headTags.contains(child.getName())) + return true; + else + return false; + } + + // dt and dd (in dl) + if (parent.getName().equals("dt") && child.getName().equals("dd")) + return false; + if (parent.getName().equals("dd") && child.getName().equals("dt")) + return false; + + return true; + } + + + // TODO - LineCountingTokenQueue + // these line numbers are an inaccurate estimate + + private void lines(Node node, String data) { + linecount += (LINE_SEPARATOR.split(data).length); + node.attr(LINE_NUMBER_ATTRIBUTE, String.valueOf(linecount)); + } + + private void whitespace() { + if (tq.peek().equals(Character.LINE_SEPARATOR)) linecount++; + tq.consumeWhitespace(); + } + + + private void annotate(Node n) { + if (null != pendingAnnotation) { + pendingAnnotation.apply(n); + pendingAnnotation = null; + } + } + +} + diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/HtmlTemplateCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/HtmlTemplateCompiler.java new file mode 100644 index 00000000..db46e249 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/HtmlTemplateCompiler.java @@ -0,0 +1,673 @@ +package com.google.sitebricks.compiler; + +import static com.google.sitebricks.compiler.AnnotationNode.ANNOTATION; +import static com.google.sitebricks.compiler.AnnotationNode.ANNOTATION_CONTENT; +import static com.google.sitebricks.compiler.AnnotationNode.ANNOTATION_KEY; +import static com.google.sitebricks.compiler.HtmlParser.LINE_NUMBER_ATTRIBUTE; +import static com.google.sitebricks.compiler.HtmlParser.SKIP_ATTR; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +import net.jcip.annotations.NotThreadSafe; + +import org.apache.commons.lang.Validate; +import org.jetbrains.annotations.NotNull; +import org.jsoup.nodes.Attribute; +import org.jsoup.nodes.Attributes; +import org.jsoup.nodes.Comment; +import org.jsoup.nodes.DataNode; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.nodes.TextNode; +import org.jsoup.nodes.XmlDeclaration; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.conversion.generics.Generics; +import com.google.sitebricks.rendering.Strings; +import com.google.sitebricks.rendering.control.Chains; +import com.google.sitebricks.rendering.control.WidgetChain; +import com.google.sitebricks.rendering.control.WidgetRegistry; +import com.google.sitebricks.routing.PageBook; +import com.google.sitebricks.routing.SystemMetrics; + +/** + * @author Shawn based on XMLTemplateCompiler by Dhanji R. Prasanna (dhanji@gmail.com) + * + */ +@NotThreadSafe +class HtmlTemplateCompiler { + private final Class page; + private final WidgetRegistry registry; + private final PageBook pageBook; + private final SystemMetrics metrics; + + private final List errors = Lists.newArrayList(); + private final List warnings = Lists.newArrayList(); + + //state variables + private Element form; + private final Stack lexicalScopes = new Stack(); + + + //special widget types (built-in symbol table) + private static final String REQUIRE_WIDGET = "@require"; + private static final String REPEAT_WIDGET = "repeat"; + private static final String CHOOSE_WIDGET = "choose"; + + public HtmlTemplateCompiler(Class page, + EvaluatorCompiler compiler, + WidgetRegistry registry, + PageBook pageBook, + SystemMetrics metrics) { + this.page = page; + this.registry = registry; + this.pageBook = pageBook; + this.metrics = metrics; + + this.lexicalScopes.push(compiler); + } + + public Renderable compile(String template) { + WidgetChain widgetChain; + widgetChain = walk(HtmlParser.parse(template)); + + // TODO - get the errors when !(isValid) + if (!errors.isEmpty() || !warnings.isEmpty()) { + // If there were any errors we must track them. + metrics.logErrorsAndWarnings(page, errors, warnings); + + // Only explode if there are errors. + if (!errors.isEmpty()) + throw new TemplateCompileException(page, template, errors, warnings); + } + + return widgetChain; + } + + private WidgetChain walk(List nodes) { + WidgetChain chain = Chains.proceeding(); + + for (Node n: nodes) + chain.addWidget(widgetize(n, walk(n))); + + return chain; + } + + /** + * Walks the DOM recursively, and converts elements into corresponding sitebricks widgets. + */ + @NotNull + private WidgetChain walk(N node) { + + WidgetChain widgetChain = Chains.proceeding(); + for (Node n: node.childNodes()) { + if (n instanceof Element) { + final Element child = (Element) n; + + //push form if this is a form tag + if (child.tagName().equals("form")) + form = (Element) n; + + //setup a lexical scope if we're going into a repeat widget (by reading the previous node) + final boolean shouldPopScope = lexicalClimb(child); + + //continue recursing down, perform a post-order, depth-first traversal of the DOM + WidgetChain childsChildren; + try { + childsChildren = walk(child); + + //process the widget itself into a Renderable with child tree + widgetChain.addWidget(widgetize(child, childsChildren)); + } finally { + lexicalDescend(child, shouldPopScope); + } + + } else if (n instanceof TextNode) { + TextNode child = (TextNode)n; + Renderable textWidget = null; + + //setup a lexical scope if we're going into a repeat widget (by reading the previous node) + final boolean shouldPopScope = lexicalClimb(child); + + // construct the text widget + try { + textWidget = registry.textWidget(cleanHtml(n), lexicalScopes.peek()); + + // if there are no annotations, add the text widget to the chain + if (!child.hasAttr(ANNOTATION_KEY)) { + widgetChain.addWidget(textWidget); + } + else { + // construct a new widget chain for this text node + WidgetChain childsChildren = Chains.proceeding().addWidget(textWidget); + + // make a new widget for the annotation, making the text chain the child + String widgetName = child.attr(ANNOTATION_KEY).toLowerCase(); + Renderable annotationWidget = registry.newWidget(widgetName, child.attr(ANNOTATION_CONTENT), childsChildren, lexicalScopes.peek()); + widgetChain.addWidget(annotationWidget); + } + + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(node.outerHtml()) + .near(line(node)) + .causedBy(e) + ); + } + + if (shouldPopScope) + lexicalScopes.pop(); + + } else if ((n instanceof Comment) || (n instanceof DataNode)) { + //process as raw text widget + try { + widgetChain.addWidget(registry.textWidget(cleanHtml(n), lexicalScopes.peek())); + } catch (ExpressionCompileException e) { + + errors.add( + CompileError.in(node.outerHtml()) + .near(line(node)) + .causedBy(e) + ); + } + } else if (n instanceof XmlDeclaration) { + try { + widgetChain.addWidget(registry + .xmlDirectiveWidget(((XmlDeclaration)n).getWholeDeclaration(), + lexicalScopes.peek())); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(node.outerHtml()) + .near(line(node)) + .causedBy(e) + ); + + } + } + } + + //return computed chain, or a terminal + return widgetChain; + } + + + /** + * Complement of HtmlTemplateCompiler#lexicalClimb(). + * This method pops off the stack of lexical scopes when + * we're done processing a sitebricks widget. + */ + private void lexicalDescend(Element element, boolean shouldPopScope) { + + //pop form + if ("form".equals(element.tagName())) + form = null; + + //pop compiler if the scope ends + if (shouldPopScope) { + lexicalScopes.pop(); + } + } + + + /** + * Called to push a new lexical scope onto the stack. + */ + private boolean lexicalClimb(Node node) { + if (node.attr(ANNOTATION).length()>1) { + + // Setup a new lexical scope (symbol table changes on each scope encountered). + if (REPEAT_WIDGET.equalsIgnoreCase(node.attr(ANNOTATION_KEY)) + || CHOOSE_WIDGET.equalsIgnoreCase(node.attr(ANNOTATION_KEY))) { + + String[] keyAndContent = {node.attr(ANNOTATION_KEY), node.attr(ANNOTATION_CONTENT)}; + lexicalScopes.push(new MvelEvaluatorCompiler(parseRepeatScope(keyAndContent, node))); + return true; + } + + // Setup a new lexical scope for compiling against embedded pages (closures). + final PageBook.Page embed = pageBook.forName(node.attr(ANNOTATION_KEY)); + if (null != embed) { + final Class embedClass = embed.pageClass(); + MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(embedClass); + checkEmbedAgainst(compiler, Parsing.toBindMap(node.attr(ANNOTATION_CONTENT)), + embedClass, node); + + lexicalScopes.push(compiler); + return true; + } + } + + return false; + } + + /** + * This method converts an XML element into a specific kind of widget. + * Special cases are the XML widget, Header, @Require widget. Otherwise a standard + * widget is created. + */ + @SuppressWarnings({"JavaDoc"}) @NotNull + private Renderable widgetize(N node, WidgetChain childsChildren) { + if (node instanceof XmlDeclaration) { + try { + XmlDeclaration decl = (XmlDeclaration)node; + return registry.xmlDirectiveWidget(decl.getWholeDeclaration(), lexicalScopes.peek()); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(node.outerHtml()) + .near(line(node)) + .causedBy(e) + ); + } + } + + // Header widget is a special case, where we match by the name of the tag =( + if ("head".equals(node.nodeName())) { + try { + return registry.headWidget(childsChildren, parseAttribs(node.attributes()), lexicalScopes.peek()); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(node.outerHtml()) + .near(line(node)) + .causedBy(e) + ); + + } + } + + String annotation = node.attr(ANNOTATION); + + //if there is no annotation, treat as a raw xml-widget (i.e. tag) + if ((null == annotation) || 0 == annotation.trim().length()) + try { + checkUriConsistency(node); + checkFormFields(node); + + return registry.xmlWidget(childsChildren, node.nodeName(), parseAttribs(node.attributes()), + lexicalScopes.peek()); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(node.outerHtml()) + .near(line(node)) + .causedBy(e) + ); + + return Chains.terminal(); + } + + // Special case: is this annotated with @Require + // if so, tags in head need to be promoted to head of enclosing page. + if (REQUIRE_WIDGET.equalsIgnoreCase(annotation.trim())) + try { + return registry.requireWidget(cleanHtml(node), lexicalScopes.peek()); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(node.outerHtml()) + .near(line(node)) + .causedBy(e) + ); + + return Chains.terminal(); + } + + // If this is NOT a self-rendering widget, give it a child. + // final String widgetName = node.attr(ANNOTATION_KEY).trim().toLowerCase()); + final String widgetName = node.attr(ANNOTATION_KEY).toLowerCase(); + + if (!registry.isSelfRendering(widgetName)) + try { + childsChildren = Chains.singleton(registry.xmlWidget(childsChildren, node.nodeName(), + parseAttribs(node.attributes()), lexicalScopes.peek())); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(node.outerHtml()) + .near(line(node)) + .causedBy(e) + ); + } + + + // Recursively build widget from [Key, expression, child widgets]. + try { + return registry.newWidget(widgetName, node.attr(ANNOTATION_CONTENT), childsChildren, lexicalScopes.peek()); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(node.outerHtml()) + .near(line(node)) + .causedBy(e) + ); + + // This should never be used. + return Chains.terminal(); + } + } + + + + + private Map parseRepeatScope(String[] extract, Node node) { + RepeatToken repeat = registry.parseRepeat(extract[1]); + Map context = Maps.newHashMap(); + + // Verify that @Repeat was parsed correctly. + if (null == repeat.var()) { + errors.add( + CompileError.in(node.outerHtml()) + .near(node.siblingIndex()) // TODO - line number + .causedBy(CompileErrors.MISSING_REPEAT_VAR) + ); + } + if (null == repeat.items()) { + errors.add( + CompileError.in(node.outerHtml()) + .near(node.siblingIndex()) // TODO - line number + .causedBy(CompileErrors.MISSING_REPEAT_ITEMS) + ); + } + + try { + Type egressType = lexicalScopes.peek().resolveEgressType(repeat.items()); + + // convert to collection if we need to + Type elementType; + Class egressClass = Generics.erase(egressType); + if (egressClass.isArray()) { + elementType = Generics.getArrayComponentType(egressType); + } + else if (Collection.class.isAssignableFrom(egressClass)) { + elementType = Generics.getTypeParameter(egressType, Collection.class.getTypeParameters()[0]); + } + else { + errors.add( + CompileError.in(node.outerHtml()) + .near(node.siblingIndex()) // TODO - line number + .causedBy(CompileErrors.REPEAT_OVER_ATOM) + ); + return Collections.emptyMap(); + } + + context.put(repeat.var(), elementType); + context.put(repeat.pageVar(), page); + context.put("index", int.class); + context.put("isLast", boolean.class); + + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(node.outerHtml()) + .near(node.siblingIndex()) // TODO - line number + .causedBy(e) + ); + } + + return context; + } + + + + + private void checkFormFields(Node element) { + if (null == form) + return; + + String action = form.attr("action"); + + // Only look at contextual uris (i.e. hosted by us). + // TODO - relative, not starting with '/' + if (null == action || (!action.startsWith("/"))) + return; + + final PageBook.Page page = pageBook.get(action); + + // Only look at pages we actually have registered. + if (null == page) { + warnings.add( + CompileError.in(element.outerHtml()) + .near(line(element)) + .causedBy(CompileErrors.UNRESOLVABLE_FORM_ACTION) + ); + + return; + } + + // If we're inside a form do a throw-away compile against the target page. + if ("input".equals(element.nodeName()) || "textarea".equals(element.nodeName())) { + String name = element.attr("name"); + + // Skip submits and buttons. + if (skippable(element.attr("type"))) + return; + + //TODO Skip empty? + if (null == name) { + warnings.add( + CompileError.in(element.outerHtml()) + .near(line(element)) + .causedBy(CompileErrors.FORM_MISSING_NAME) + ); + + return; + } + + // Compile expression path. + final String expression = name; + try { + new MvelEvaluatorCompiler(page.pageClass()) + .compile(expression); + + } catch (ExpressionCompileException e) { + //TODO Very hacky, needed to strip out xmlns attribution. + warnings.add( + CompileError.in(element.outerHtml()) + .near(element.siblingIndex()) // TODO - line number + .causedBy(CompileErrors.UNRESOLVABLE_FORM_BINDING, e) + ); + } + + } + + } + + private void checkUriConsistency(Node element) { + String uriAttrib = element.attr("action"); + if (null == uriAttrib) + uriAttrib = element.attr("src"); + if (null == uriAttrib) + uriAttrib = element.attr("href"); + + if (null != uriAttrib) { + + // Verify that such a uri exists in the page book, + // only if it is contextual--ignore abs & relative URIs. + final String uri = uriAttrib; + if (uri.startsWith("/")) + if (null == pageBook.nonCompilingGet(uri)) + warnings.add( + CompileError.in(element.outerHtml()) + .near(element.siblingIndex()) // TODO - line number + .causedBy(CompileErrors.UNRESOLVABLE_FORM_ACTION, uri) + ); + } + } + + + + + /** + * @param attributes A list of attribs + * @return Returns a mutable map parsed out of the attribute list + */ + static Map parseAttribs(Attributes attributes) { + + Map attrs = new LinkedHashMap(attributes.size() + 4); + + for (Attribute a : attributes.asList()) + if (SKIP_ATTR.contains(a.getKey())) + continue; + else + attrs.put(a.getKey(), a.getValue()); + + return attrs; + } + + // Ensures that embed bound properties are writable + private void checkEmbedAgainst(EvaluatorCompiler compiler, Map properties, + Class embedClass, Node node) { + + // TODO also type check them against expressions + for (String property : properties.keySet()) { + try { + if (!compiler.isWritable(property)) { + errors.add( + CompileError.in(node.outerHtml()) + //TODO we need better line number detection if there is whitespace between the annotation and tag. + .near(node.siblingIndex()-1) // TODO - line number of the annotation + .causedBy(CompileErrors.PROPERTY_NOT_WRITEABLE, + String.format("Property %s#%s was not writable. Did you forget to create " + + "a setter or @Visible annotation?", embedClass.getSimpleName(), property)) + ); + } + } catch (ExpressionCompileException ece) { + errors.add( + CompileError.in(node.outerHtml()) + .near(node.siblingIndex()) // TODO - line number + .causedBy(CompileErrors.ERROR_COMPILING_PROPERTY) + ); + } + } + } + + + static boolean skippable(String kind) { + if (null == kind) + return false; + + return ("submit".equals(kind) + || "button".equals(kind) + || "reset".equals(kind) + || "file".equals(kind)); + } + + + + + + // TESTING jsoup.nodes.Node + + /** + Get this node's previous sibling. + @return the previous sibling, or null if this is the first sibling + */ + public Node previousSibling(Node node) { + Validate.notNull(node); + + List siblings = findSiblings(node); + if (null == siblings) return null; + + Integer index = indexInList(node, siblings); + if (null == index) return null; + + if (index > 0) + return siblings.get(index-1); + + return null; + } + + public List findSiblings(Node node) { + Validate.notNull(node); + + Node parent = node.parent(); + if (null == parent) return null; + + return parent.childNodes(); + } + + /** + * Get the list index of this node in its node sibling list. I.e. if this is the first node + * sibling, returns 0. + * @return position in node sibling list + * @see org.jsoup.nodes.Element#elementSiblingIndex() + */ + + public Integer siblingIndex(Node node) { + if (null != node.parent()) + Validate.notNull(node); + return indexInList(node, findSiblings(node)); + } + + protected static Integer indexInList(N search, List nodes) { + Validate.notNull(search); + Validate.notNull(nodes); + + for (int i = 0; i < nodes.size(); i++) { + N node = nodes.get(i); + if (node.equals(search)) + return i; + } + return null; + } + + private static int line(Node node) { + return Integer.valueOf(node.attr(LINE_NUMBER_ATTRIBUTE)); + } + + // outerHtml from jsoup.Node, Element with suppressed _attribs + + private static String cleanHtml(final Node node) { + if (node instanceof Element) { + Element element = ((Element) node); + StringBuilder accum = new StringBuilder(); + accum.append("<").append(element.tagName()); + for (Attribute attribute: element.attributes()) { + if (!(attribute.getKey().startsWith("_"))) { + accum.append(" "); + accum.append(attribute.getKey()); + accum.append("=\""); + accum.append(attribute.getValue()); + accum.append('"'); + } + } + + if (element.childNodes().isEmpty() && element.tag().isEmpty()) { + accum.append(" />"); + } else { + accum.append(">"); + for (Node child : element.childNodes()) + accum.append(cleanHtml(child)); + + accum.append(""); + } + return accum.toString(); + } else if (node instanceof TextNode) { + return ((TextNode) node).getWholeText(); + } else if (node instanceof XmlDeclaration) { + + // HACK + if (node.childNodes().isEmpty()) { + return ""; + } + return node.outerHtml(); + } else if (node instanceof Comment) { + // HACK: elide comments for now. + return ""; + } else if (node instanceof DataNode && node.childNodes().isEmpty()) { + // No child nodes are defined but we have to handle content if such exists, example + // + + String content = node.attr("data"); + if (Strings.empty(content)) { + return ""; + } + + return content; + } else { + return node.outerHtml(); + } + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/MvelEvaluatorCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/MvelEvaluatorCompiler.java new file mode 100644 index 00000000..73603d90 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/MvelEvaluatorCompiler.java @@ -0,0 +1,225 @@ +package com.google.sitebricks.compiler; + +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.jcip.annotations.NotThreadSafe; + +import org.jetbrains.annotations.Nullable; +import org.mvel2.CompileException; +import org.mvel2.MVEL; +import org.mvel2.ParserContext; +import org.mvel2.compiler.CompiledExpression; +import org.mvel2.compiler.ExpressionCompiler; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.Visible; +import com.google.sitebricks.conversion.generics.Generics; +import com.google.sitebricks.conversion.generics.ParameterizedTypeImpl; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + * + * TODO make this thread-safe when pages can be compiled on demand + */ +@NotThreadSafe +public class MvelEvaluatorCompiler implements EvaluatorCompiler { + private final Class backingType; + private final Map backingTypes; + + private static final String CLASS = "class"; + private final Set writeableProperties = Sets.newHashSet(); + private final Map egressTypes = Maps.newHashMap(); + private ParserContext cachedParserContext; + + public MvelEvaluatorCompiler(Class backingType) { + this.backingType = backingType; + this.backingTypes = null; + } + + public MvelEvaluatorCompiler(Map backingTypes) { + this.backingTypes = Collections.unmodifiableMap(backingTypes); + this.backingType = null; + } + + //memo field caches compiled expressions + private final Map compiled = + new HashMap(); + + + public Type resolveEgressType(String expression) throws ExpressionCompileException { + + // try to get the type from the cache + Type type = egressTypes.get(expression); + if (type != null) { + return type; + } + + CompiledExpression compiled = compileExpression(expression); + final Class egressClass = compiled.getKnownEgressType(); + final Type[] parameters = compiled.getParserContext().getLastTypeParameters(); + + if (parameters == null) { + // the class is not parameterised (generic) + type = egressClass; + } + else { + // reconstruct the Type from mvel's generics details + type = new ParameterizedTypeImpl(egressClass, parameters, egressClass.getEnclosingClass()); + } + + egressTypes.put(expression, type); + + return type; + } + + public boolean isWritable(String property) throws ExpressionCompileException { + // Ensure we have introspected. Relying on sidefx, ugh. + getParserContext(); + + return writeableProperties.contains(property); + } + + public Evaluator compile(String expression) throws ExpressionCompileException { + + //do *not* inline + final CompiledExpression compiled = compileExpression(expression); + + return new Evaluator() { + @Nullable + public Object evaluate(String expr, Object bean) { + return MVEL.executeExpression(compiled, bean); + } + + public void write(String expr, Object bean, Object value) { + //lets use mvel to store an expression + MVEL.setProperty(bean, expr, value); + } + + public Object read(String property, Object contextObject) { + return MVEL.getProperty(property, contextObject); + } + }; + } + + private CompiledExpression compileExpression(String expression) + throws ExpressionCompileException { + final CompiledExpression compiledExpression = compiled.get(expression); + + //use cached copy + if (null != compiledExpression) + return compiledExpression; + + //otherwise compile expression and cache + final ExpressionCompiler compiler = new ExpressionCompiler(expression, getParserContext()); + + CompiledExpression tempCompiled; + try { + tempCompiled = compiler.compile(); + } catch (CompileException ce) { + throw new ExpressionCompileException(expression, ce.getErrors()); + } + + //store in memo cache + compiled.put(expression, tempCompiled); + + return tempCompiled; + } + + private ParserContext getParserContext() throws ExpressionCompileException { + if (null != cachedParserContext) { + return cachedParserContext; + } + + return cachedParserContext = (null != backingType) + ? singleBackingTypeParserContext() : backingMapParserContext(); + } + + + @SuppressWarnings({ "unchecked", "rawtypes" }) +private ParserContext backingMapParserContext() { + ParserContext context = new ParserContext(); + context.setStrongTyping(true); + + context.addInputs((Map) backingTypes); + + return context; + } + + public List tokenizeAndCompile(String template) throws ExpressionCompileException { + return Parsing.tokenize(template, this); + } + + //generates a parsing context with type information from the backing type's javabean properties + private ParserContext singleBackingTypeParserContext() throws ExpressionCompileException { + ParserContext context = new ParserContext(); + context.setStrongTyping(true); + context.addInput("this", backingType); + + PropertyDescriptor[] propertyDescriptors; + try { + propertyDescriptors = Introspector.getBeanInfo(backingType).getPropertyDescriptors(); + } catch (IntrospectionException e) { + throw new ExpressionCompileException("Could not read class " + backingType); + } + + // read @Visible annotated fields. + for (Field field : backingType.getDeclaredFields()) { + if (field.isAnnotationPresent(Visible.class)) { + context.addInput(field.getName(), field.getType()); + + if (!field.getAnnotation(Visible.class).readOnly()) { + writeableProperties.add(field.getName()); + } + } + } + + // read javabean properties -- these override @Visible fields. + for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { + // skip getClass() + if (CLASS.equals(propertyDescriptor.getName())) + continue; + + if (null != propertyDescriptor.getWriteMethod()) { + writeableProperties.add(propertyDescriptor.getName()); + } + + // if this is a collection, determine its type parameter + if (Collection.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { + + Type propertyType; + if (propertyDescriptor.getReadMethod() != null) { + propertyType = propertyDescriptor.getReadMethod().getGenericReturnType(); + } + else { + propertyType = propertyDescriptor.getWriteMethod().getGenericParameterTypes()[0]; + } + + ParameterizedType collectionType = (ParameterizedType) Generics + .getExactSuperType(propertyType, Collection.class); + + Class[] parameterClasses = new Class[1]; + Type parameterType = collectionType.getActualTypeArguments()[0]; + parameterClasses[0] = Generics.erase(parameterType); + + context.addInput(propertyDescriptor.getName(), propertyDescriptor.getPropertyType(), parameterClasses); + } else { + context.addInput(propertyDescriptor.getName(), propertyDescriptor.getPropertyType()); + } + } + + return context; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/Parsing.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/Parsing.java new file mode 100644 index 00000000..0e341016 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/Parsing.java @@ -0,0 +1,262 @@ +package com.google.sitebricks.compiler; + + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import com.google.inject.Inject; +import com.google.sitebricks.conversion.TypeConverter; +import com.google.sitebricks.rendering.Strings; + +/** + * Utility tokenizes text into expressions and raw text, and provides other + * text parsing tools. + * + * @author Dhanji R. Prasanna (dhanji at gmail com) + * @since 1.0 + */ +public class Parsing { + + private static TypeConverter converter; + + private Parsing() { + } + + //converts comma-separated name/value pairs into expression/variable bindings + public static Map toBindMap(String expression) { + if (Strings.empty(expression)) + return Collections.emptyMap(); + + Deque escapes = new ArrayDeque(); + List pairs = new ArrayList(); + int index = 0; + for (int i = 0; i < expression.length(); i++) { + char c = expression.charAt(i); + + if (('"' == c && (escapes.isEmpty() || escapes.peek().charValue() != c)) + || '[' == c || '{' == c || '(' == c) { + escapes.push(c); + } else if (('"' == c && escapes.peek().charValue() == c) || ']' == c || '}' == c || ')' == c) { + escapes.pop(); + } + + if (escapes.isEmpty() && ',' == c) { + if (index < i) + pairs.add(expression.substring(index, i)); + + //skip comma & whitespace if any + for (; i < expression.length() && (',' == expression.charAt(i) || ' ' == expression.charAt(i));) + i++; + + //reset new start index + index = i; + } + + } + + //add last pair if needed + if (index < expression.length()) { + + //chew up leading comma & whitespace if any + //noinspection StatementWithEmptyBody + for (; ',' == expression.charAt(index) || ' ' == expression.charAt(index); index++) ; + + final String pair = expression.substring(index, expression.length()).trim(); + + //only consider this a pair if it has something in it! + if (pair.length() > 1) + pairs.add(pair); + } + + //nice to preserve insertion order + final Map map = new LinkedHashMap(); + for (String pair : pairs) { + final String[] nameAndValue = pair.split("=", 2); + + //do some validation + if (nameAndValue.length != 2) + throw new IllegalArgumentException("Invalid parameter binding format: " + pair); + + Strings.nonEmpty(nameAndValue[0], "Cannot have an empty left hand side target parameter: " + pair); + Strings.nonEmpty(nameAndValue[1], "Must provide a non-empty right hand side expression: " + pair); + + map.put(nameAndValue[0].trim(), nameAndValue[1].trim()); + } + + return Collections.unmodifiableMap(map); + } + + + //tokenizes text into raw text chunks interspersed with expression chunks + public static List tokenize(String warpRawText, EvaluatorCompiler compiler) throws ExpressionCompileException { + ArrayList tokens = new ArrayList(); + + //simple state machine to iterate the text and break it up into chunks + char[] characters = warpRawText.toCharArray(); + + StringBuilder token = new StringBuilder(); + TokenizerState state = TokenizerState.READING_TEXT; + for (int i = 0; i < characters.length; i++) { + + //test for start of an expression + if (TokenizerState.READING_TEXT.equals(state)) { + if ('$' == characters[i]) { + if ('{' == characters[i + 1]) { + //YES it is the start of an expr, so close up the existing token & start a new one + if (token.length() > 0) { + tokens.add(CompiledToken.text(token.toString())); + token = new StringBuilder(); + } + + state = TokenizerState.READING_EXPRESSION; + } + } + } + + //test for end of an expr + if (TokenizerState.READING_EXPRESSION.equals(state)) { + if ('}' == characters[i]) { + //YES it is the end of the expr, so close it up and start a new token + token.append(characters[i]); + + tokens.add(CompiledToken.expression(token.toString(), compiler)); + token = new StringBuilder(); + + state = TokenizerState.READING_TEXT; + continue; //dont add the trailing } to the new text field + } + } + + //add characters to the token normally + token.append(characters[i]); + } + + //should never be in reading expr mode at this point + if (TokenizerState.READING_EXPRESSION.equals(state)) + throw new IllegalStateException("Error. Expression was not terminated properly: " + token.toString()); + + //add last token read if it has any content (is always text) + if (token.length() > 0) + tokens.add(CompiledToken.text(token.toString())); + + // Pack list capacity to size (saves memory). + tokens.trimToSize(); + + return tokens; + } + + public static String stripExpression(String expr) { + return expr.substring(2, expr.length() - 1); + } + + public static boolean isExpression(String attribute) { + return attribute.startsWith("${"); + } + + + //dont pass null or empty string or 1 char + public static String stripQuotes(String var) { + return var.substring(1, var.length() - 1); + } + + /** + * Remember this method is not so much about verifying something is XML as it is + * verifying that something is a NON-Xml template. In other words, read this as + * whether or not we should *treat* something as XML, then complain that it's malformed + * later (if necessary). + * + * @param template A fully loaded template as a string. + * @return Returns true if this template should be treated as an XML template. + * Templates that are not XML *MUST* begin with a {@code @Meta} annotation. + */ + public static boolean treatAsXml(String template) { + return 0 > indexOfMeta(template); + } + + /** + * Converts the given token stream into a rendered output evaluating each expression + * against the provided context object which may be a regular Java POJO with getters + * and setters or a map of string/value pairs. + */ + public static String render(List tokens, Map arguments) { + StringBuilder builder = new StringBuilder(); + for (Token token : tokens) { + builder.append(token.render(arguments)); + } + + return builder.toString(); + } + + public static int indexOfMeta(String template) { + //do a manual character scan (coz indexOf(regex) will be O(n) runtime) + for (int i = 0; i < template.length(); i++) { + char c = template.charAt(i); + + //skip leading whitespace + if (isWhitespace(c)) + continue; + + //Does this template begin with @Meta or @Meta( --> then it is *not* XML + if ('@' == c) { + final char trailing = template.charAt(i + 5); + + if ("Meta".equals(template.substring(i + 1, i + 5)) + + && ('(' == trailing || isWhitespace(trailing))) + + return i; + } + + //do not go past the first non-whitespace character (short-circuit) + return -1; + } + + //treat everything else as XML + return -1; + } + + private static boolean isWhitespace(char c) { + return ' ' == c || '\n' == c || '\r' == c || '\t' == c; + } + + private static enum TokenizerState { + READING_TEXT, READING_EXPRESSION + } + + //URI test regex: (([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)?/{0,2}[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)?(#[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)? + //Taken from stylus studio message board http://www.stylusstudio.com/xmldev/200108/post10890.html + + private final static Pattern URI_REGEX + = Pattern.compile("(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)?/{0,2}[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)?(#[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)?"); + +// "(([a-zA-Z][0-9a-zA-Z+\\\\-\\\\.]*:)?/{0,2}[0-9a-zA-Z;" + +// "/?:@&=+$\\\\.\\\\-_!~*'()%]+)?(#[0-9a-zA-Z;/?:@&=+$\\\\.\\\\-_!~*'()%]+)?"); + + //TODO + private final static Pattern TEMPLATE_URI_PATTERN = Pattern.compile("(([a-zA-Z][0-9a-zA-Z+\\\\-\\\\.]*:)?/{0,2}[0-9a-zA-Z;" + + "/?:@&=+$\\\\.\\\\-_!~*'()%]+)?(#[0-9a-zA-Z;/?:@&=+$\\\\.\\\\-_!~*'()%]+)?"); + + + //less expensive method tests whether string is a valid URI + public static boolean isValidURI(String uri) { + return (null != uri) + && URI_REGEX + .matcher(uri) + .matches(); + } + + public static TypeConverter getTypeConverter() { + return Parsing.converter; + } + + @Inject + public static void setTypeConverter(TypeConverter converter) { + Parsing.converter = converter; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/RepeatToken.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/RepeatToken.java new file mode 100644 index 00000000..5d4f6a92 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/RepeatToken.java @@ -0,0 +1,15 @@ +package com.google.sitebricks.compiler; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public interface RepeatToken { + String VAR = "var"; + String PAGE_VAR = "pageVar"; + String ITEMS = "items"; + String DEFAULT_PAGEVAR = "__page"; + + String items(); + String var(); + String pageVar(); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/RequireWidgetInternPool.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/RequireWidgetInternPool.java new file mode 100644 index 00000000..2863b1d1 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/RequireWidgetInternPool.java @@ -0,0 +1,13 @@ +package com.google.sitebricks.compiler; + +import com.google.inject.Singleton; + +/** + * A singleton intern pool used to manage similarities between @Require widgets. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Singleton //@Concurrent +class RequireWidgetInternPool { + +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java new file mode 100644 index 00000000..087c53ea --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java @@ -0,0 +1,158 @@ +package com.google.sitebricks.compiler; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.sitebricks.Bricks; +import com.google.sitebricks.MissingTemplateException; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Show; +import com.google.sitebricks.Template; +import com.google.sitebricks.TemplateLoader; +import com.google.sitebricks.compiler.template.MvelTemplateCompiler; +import com.google.sitebricks.compiler.template.freemarker.FreemarkerTemplateCompiler; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.rendering.Decorated; +import com.google.sitebricks.rendering.control.WidgetRegistry; +import com.google.sitebricks.routing.PageBook; +import com.google.sitebricks.routing.SystemMetrics; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Map; + +/** + * A factory for internal template compilers. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@Singleton +class StandardCompilers implements Compilers { + private final WidgetRegistry registry; + private final PageBook pageBook; + private final SystemMetrics metrics; + private final Map> httpMethods; + private final TemplateLoader loader; + + @Inject + public StandardCompilers(WidgetRegistry registry, PageBook pageBook, SystemMetrics metrics, + @Bricks Map> httpMethods, TemplateLoader loader) { + this.registry = registry; + this.pageBook = pageBook; + this.metrics = metrics; + this.httpMethods = httpMethods; + this.loader = loader; + } + + public Renderable compileXml(Class page, String template) { + return new XmlTemplateCompiler(page, new MvelEvaluatorCompiler(page), registry, pageBook, + metrics) + .compile(template); + } + + public Renderable compileHtml(Class page, String template) { + return new HtmlTemplateCompiler(page, new MvelEvaluatorCompiler(page), registry, pageBook, + metrics) + .compile(template); + } + + public Renderable compileFlat(Class page, String template) { + return new FlatTemplateCompiler(page, new MvelEvaluatorCompiler(page), metrics, registry) + .compile(template); + } + + public Renderable compileMvel(Class page, String template) { + return new MvelTemplateCompiler(page).compile(template); + } + + public Renderable compileFreemarker( Class page, String template ) { + return new FreemarkerTemplateCompiler(page).compile(template); + } + + // TODO(dhanji): Feedback errors as return rather than throwing. + public void analyze(Class page) { + // May move this into a separate class if it starts getting too big. + analyzeMethods(page.getDeclaredMethods()); + analyzeMethods(page.getMethods()); + } + + private void analyzeMethods(Method[] methods) { + for (Method method : methods) { + for (Annotation annotation : method.getDeclaredAnnotations()) { + // if this is a http method annotation, do some checking on the + // args and return types. + if (httpMethods.containsValue(annotation.annotationType())) { + Class returnType = method.getReturnType(); + + PageBook.Page page = pageBook.forClass(returnType); + if (null == page) { + // throw an error. + } else { + // do further analysis on this sucka + if (page.getUri().contains(":")) + ; // throw an error coz we cant redir to dynamic URLs + + + // If this is headless, it MUST return an instance of reply. + if (page.isHeadless()) { + if (!Reply.class.isAssignableFrom(method.getReturnType())) { + // throw error + } + } + } + } + } + } + } + + public void compilePage(PageBook.Page page) { + // find the template page class + Class templateClass = page.pageClass(); + + // root page uses the last template, extension uses its own embedded template + if (!page.isDecorated() && templateClass.isAnnotationPresent(Decorated.class)) { + // the first superclass with a @Show and no @Extension is the template + while (!templateClass.isAnnotationPresent(Show.class) || + templateClass.isAnnotationPresent(Decorated.class)) { + templateClass = templateClass.getSuperclass(); + if (templateClass == Object.class) { + throw new MissingTemplateException("Could not find tempate for " + page.pageClass() + + ". You must use @Show on a superclass of an @Extension page"); + } + } + } + + Renderable widget = compile(templateClass); + + //apply the compiled widget chain to the page (completing compile step) + page.apply(widget); + } + + @Override + public Renderable compile(Class templateClass) { + final Template template = loader.load(templateClass); + + Renderable widget; + + //is this an HTML, XML, or a flat-file template? + switch(template.getKind()) { + default: + case HTML: + widget = compileHtml(templateClass, template.getText()); + break; + case XML: + widget = compileXml(templateClass, template.getText()); + break; + case FLAT: + widget = compileFlat(templateClass, template.getText()); + break; + case MVEL: + widget = compileMvel(templateClass, template.getText()); + break; + case FREEMARKER: + widget = compileFreemarker(templateClass, template.getText()); + break; + } + return widget; + } + +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/TemplateCompileException.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/TemplateCompileException.java new file mode 100644 index 00000000..92f6a26c --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/TemplateCompileException.java @@ -0,0 +1,130 @@ +package com.google.sitebricks.compiler; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public final class TemplateCompileException extends RuntimeException { + private final List errors; + private final List templateLines; + private final String template; + private final Class page; + private final List warnings; + + public TemplateCompileException(Class page, String template, + List errors, List warnings) { + + this.page = page; + this.warnings = warnings; + try { + //noinspection unchecked + this.templateLines = IOUtils.readLines(new StringReader(template)); + } catch (IOException e) { + throw new IllegalStateException("Fatal error, could not read template after compile", e); + } + this.template = template; + + this.errors = errors; + } + + + @Override + public String getMessage() { + if (null == errors) + return super.getMessage(); + + StringBuilder builder = new StringBuilder("Compilation errors in template for "); + builder.append(page.getName()); + builder.append("\n\n"); + + AtomicInteger i = new AtomicInteger(0); + + if (!errors.isEmpty()) { + toString(builder, i, errors); + builder.append("\nTotal errors: "); + builder.append(errors.size()); + builder.append("\n\n"); + } + + if (!warnings.isEmpty()) { + toString(builder, i, warnings); + builder.append("\nTotal warnings: "); + builder.append(warnings.size()); + builder.append("\n\n"); + } + + return builder.toString(); + } + + private void toString(StringBuilder builder, AtomicInteger i, List errors) { + for (CompileError error : errors) { + EvaluatorCompiler.CompileErrorDetail cause = error.getCause(); + + builder.append(i.incrementAndGet()); + builder.append(") "); + + if (cause == null) { + builder.append("Unknown cause"); + builder.append("\n\n"); + } + else { + builder.append(cause.getError().getMessage()); + builder.append("\n\n"); + } + + // Context (source code) of the error. + int lineNumber = error.getLine(); + builder.append(lineNumber - 1); + builder.append(": "); + if (lineNumber > templateLines.size() - 1) { + continue; + } + builder.append(templateLines.get(lineNumber - 1)); + builder.append('\n'); + builder.append(lineNumber); + builder.append(": "); + builder.append(templateLines.get(lineNumber)); + builder.append('\n'); + + // Actual error line... + int contextLineNumber = lineNumber + 1; + builder.append(contextLineNumber); + builder.append(": "); + String fragment = templateLines.get(contextLineNumber); + builder.append(fragment); + builder.append('\n'); + + // Compute offset (line number width + expression offset). + int columnPad = Integer.toString(contextLineNumber).length() + 4; + int offset; + if (cause != null) + offset = fragment.indexOf(cause.getExpression()) + columnPad; + else + offset = 0; + + // Code pointer (caret). + // TODO fix this. It should appear directly beneath the line in question. + char[] spaces = new char[offset]; + Arrays.fill(spaces, ' '); + builder.append(spaces); + builder.append("^"); + + builder.append('\n'); + } + } + + public List getErrors() { + return errors; + } + + public List getWarnings() { + return warnings; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/TemplateParseException.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/TemplateParseException.java new file mode 100644 index 00000000..2c4f1013 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/TemplateParseException.java @@ -0,0 +1,10 @@ +package com.google.sitebricks.compiler; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +class TemplateParseException extends RuntimeException { + public TemplateParseException(Exception e) { + super(e); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/Token.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/Token.java new file mode 100644 index 00000000..03571d73 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/Token.java @@ -0,0 +1,28 @@ +package com.google.sitebricks.compiler; + +/** + * Represents a compiled, evaluable expression or raw String token. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public interface Token { + + /** + * + * @return Returns true if this is an evaluable expression (usually with an embedded + * MVEL evaluator). + * + */ + boolean isExpression(); + + /** + * + * @param bound A context object to evaluate against (must matched the compiled context + * class of this expression token). + * + * @return Returns the result of evaluating the expression token against the provided + * context object. Values are converted to String using the {@code TypeConverter}. + * + */ + String render(Object bound); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/XmlTemplateCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/XmlTemplateCompiler.java new file mode 100644 index 00000000..ad931ae3 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/XmlTemplateCompiler.java @@ -0,0 +1,505 @@ +package com.google.sitebricks.compiler; + +import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +import net.jcip.annotations.NotThreadSafe; + +import org.dom4j.Attribute; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.DocumentType; +import org.dom4j.Element; +import org.dom4j.Node; +import org.dom4j.io.SAXReader; +import org.jetbrains.annotations.NotNull; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.conversion.generics.Generics; +import com.google.sitebricks.rendering.control.Chains; +import com.google.sitebricks.rendering.control.WidgetChain; +import com.google.sitebricks.rendering.control.WidgetRegistry; +import com.google.sitebricks.routing.PageBook; +import com.google.sitebricks.routing.SystemMetrics; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + * + * TODO share code with HtmlTemplateCompiler + */ +@NotThreadSafe +class XmlTemplateCompiler { + private final Class page; + private final WidgetRegistry registry; + private final PageBook pageBook; + private final SystemMetrics metrics; + + private final List errors = Lists.newArrayList(); + private final List warnings = Lists.newArrayList(); + + //state variables + private Element form; + private final Stack lexicalScopes = new Stack(); + + + //special widget types (built-in symbol table) + private static final String REQUIRE_WIDGET = "@require"; + private static final String REPEAT_WIDGET = "repeat"; + private static final String CHOOSE_WIDGET = "choose"; + + + public XmlTemplateCompiler(Class page, + EvaluatorCompiler compiler, + WidgetRegistry registry, + PageBook pageBook, + SystemMetrics metrics) { + + this.page = page; + this.registry = registry; + this.pageBook = pageBook; + this.metrics = metrics; + + this.lexicalScopes.push(compiler); + } + + public Renderable compile(String template) { + WidgetChain widgetChain; + try { + final SAXReader reader = new SAXReader(); + reader.setMergeAdjacentText(true); + reader.setXMLFilter(Dom.newLineNumberFilter()); + reader.setValidation(false); + reader.setIncludeExternalDTDDeclarations(true); + + reader.setEntityResolver(new EntityResolver() { + public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { + if (systemId.contains(".dtd")) { + return new InputSource(new StringReader("")); + } else { + return null; + } + } + }); + + widgetChain = walk(reader.read(new StringReader(template))); + } catch (DocumentException e) { + errors.add( + CompileError.in(template) + .near(0) + .causedBy(CompileErrors.MALFORMED_TEMPLATE) + ); + + // Really this should only have the 1 error, but we need to set errors/warnings atomically. + metrics.logErrorsAndWarnings(page, errors, warnings); + + throw new TemplateParseException(e); + } + + if (!errors.isEmpty() || !warnings.isEmpty()) { + // If there were any errors we must track them. + metrics.logErrorsAndWarnings(page, errors, warnings); + + // Only explode if there are errors. + if (!errors.isEmpty()) + throw new TemplateCompileException(page, template, errors, warnings); + } + + return widgetChain; + } + + private WidgetChain walk(Document document) { + WidgetChain chain = Chains.proceeding(); + handleDocType(document, chain); + final WidgetChain docChain = walk(document.getRootElement()); + + chain.addWidget(widgetize(null, document.getRootElement(), docChain)); + + return chain; + } + + private void handleDocType(Document document, WidgetChain chain) { + DocumentType docType = document.getDocType(); + if (docType != null) { + String docTypeRawXml = document.getDocType().asXML(); + try { + chain.addWidget(registry.textWidget(Dom.stripAnnotation(docTypeRawXml), lexicalScopes.peek())); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(docTypeRawXml) + .causedBy(e) + ); + } + } + } + + /** + * Walks the DOM recursively, and converts elements into + * corresponding sitebricks widgets. + */ + @SuppressWarnings({"JavaDoc"}) @NotNull + private WidgetChain walk(Element element) { + + WidgetChain widgetChain = Chains.proceeding(); + + for (int i = 0, size = element.nodeCount(); i < size; i++) { + Node node = element.node(i); + + if (Dom.isElement(node)) { + final Element child = (Element) node; + + //push form if this is a form tag + if (Dom.isForm(node)) + form = (Element) node; + + + //setup a lexical scope if we're going into a repeat widget (by reading the previous node) + final boolean shouldPopScope = lexicalClimb(element, i); + + //continue recursing down, perform a post-order, depth-first traversal of the DOM + WidgetChain childsChildren; + try { + childsChildren = walk(child); + + //process the widget itself into a Renderable with child tree + if (i > 0) + widgetChain.addWidget(widgetize(element.node(i - 1), child, childsChildren)); + else + widgetChain.addWidget(widgetize(null, child, childsChildren)); + + } finally { + lexicalDescend(node, shouldPopScope); + } + + } else if (Dom.isTextCommentOrCdata(node)) { + //process as raw text widget + try { + widgetChain.addWidget(registry.textWidget(Dom.stripAnnotation(node.asXML()), lexicalScopes.peek())); + } catch (ExpressionCompileException e) { + + errors.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(e) + ); + } + } + } + + //return computed chain, or a terminal + return widgetChain; + } + + + /** + * Complement of XmlTemplateCompiler#lexicalClimb(). + * This method pops off the stack of lexical scopes when + * we're done processing a sitebricks widget. + */ + private void lexicalDescend(Node node, boolean shouldPopScope) { + + //pop form + if (Dom.isForm(node)) + form = null; + + //pop compiler if the scope ends + if (shouldPopScope) { + lexicalScopes.pop(); + } + } + + + /** + * Called to push a new lexical scope onto the stack. + */ + private boolean lexicalClimb(Element element, int i) { + //read annotation on this node only if it is not the root node + String annotation = i > 0 ? Dom.readAnnotation(element.node(i - 1)) : null; + + if (null != annotation) { + String[] keyAndContent = Dom.extractKeyAndContent(annotation); + + // Setup a new lexical scope (symbol table changes on each scope encountered). + final String name = keyAndContent[0]; + if (REPEAT_WIDGET.equalsIgnoreCase(name) || CHOOSE_WIDGET.equalsIgnoreCase(name)) { + lexicalScopes.push(new MvelEvaluatorCompiler(parseRepeatScope(keyAndContent, element))); + return true; + } + + // Setup a new lexical scope for compiling against embedded pages (closures). + final PageBook.Page embed = pageBook.forName(name); + if (null != embed) { + final Class embedClass = embed.pageClass(); + MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(embedClass); + checkEmbedAgainst(compiler, Parsing.toBindMap(keyAndContent[1]), embedClass, + (Element) element.node(i)); + lexicalScopes.push(compiler); + return true; + } + } + + return false; + } + + // Ensures that embed bound properties are writable + private void checkEmbedAgainst(EvaluatorCompiler compiler, Map properties, + Class embedClass, Element element) { + + // TODO also type check them against expressions + for (String property : properties.keySet()) { + try { + if (!compiler.isWritable(property)) { + errors.add( + CompileError.in(Dom.asRawXml(element)) + //TODO we need better line number detection if there is whitespace between the annotation and tag. + .near(Dom.lineNumberOf(element) - 1) // Really we want the line number of the annotation not the tag. + .causedBy(CompileErrors.PROPERTY_NOT_WRITEABLE, + String.format("Property %s#%s was not writable. Did you forget to create " + + "a setter or @Visible annotation?", embedClass.getSimpleName(), property)) + ); + } + } catch (ExpressionCompileException ece) { + errors.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(CompileErrors.ERROR_COMPILING_PROPERTY) + ); + } + } + } + + + /** + * This method converts an XML element into a specific kind of widget. + * Special cases are the XML widget, Header, @Require widget. Otherwise a standard + * widget is created. + */ + @SuppressWarnings({"JavaDoc"}) @NotNull + private Renderable widgetize(Node preceding, Element element, WidgetChain childsChildren) { + + // Header widget is a special case, where we match by the name of the tag =( + if ("head".equals(element.getName())) { + try { + return registry.headWidget(childsChildren, Dom.parseAttribs(element.attributes()), lexicalScopes.peek()); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(e) + ); + + } + } + + //read annotation if available + String annotation = Dom.readAnnotation(preceding); + + //if there is no annotation, treat as a raw xml-widget (i.e. tag) + if (null == annotation) + try { + checkUriConsistency(element); + checkFormFields(element); + + return registry.xmlWidget(childsChildren, element.getName(), Dom.parseAttribs(element.attributes()), + lexicalScopes.peek()); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(e) + ); + + return Chains.terminal(); + } + + // Special case: is this a "require" widget? (used for exporting + // header tags into enclosing pages). + if (REQUIRE_WIDGET.equalsIgnoreCase(annotation.trim())) + try { + + return registry.requireWidget(Dom.stripAnnotation(Dom.asRawXml(element)), lexicalScopes.peek()); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(e) + ); + + return Chains.terminal(); + } + + // Process as "normal" widget. + String[] extract = Dom.extractKeyAndContent(annotation); + + // If this is NOT a self-rendering widget, give it an XML child. + final String widgetName = extract[0].trim().toLowerCase(); + if (!registry.isSelfRendering(widgetName)) + try { + childsChildren = Chains.singleton(registry.xmlWidget(childsChildren, element.getName(), + Dom.parseAttribs(element.attributes()), lexicalScopes.peek())); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(e) + ); + } + + + + // Recursively build widget from [Key, expression, child widgets]. + try { + return registry.newWidget(widgetName, extract[1], childsChildren, lexicalScopes.peek()); + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(e) + ); + + // This should never be used. + return Chains.terminal(); + } + } + + + + + private Map parseRepeatScope(String[] extract, Element element) { + RepeatToken repeat = registry.parseRepeat(extract[1]); + Map context = Maps.newHashMap(); + + // Verify that @Repeat was parsed correctly. + if (null == repeat.var()) { + errors.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(CompileErrors.MISSING_REPEAT_VAR) + ); + } + if (null == repeat.items()) { + errors.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(CompileErrors.MISSING_REPEAT_ITEMS) + ); + } + + try { + Type egressType = lexicalScopes.peek().resolveEgressType(repeat.items()); + Type elementType = Generics.getTypeParameter(egressType, Collection.class.getTypeParameters()[0]); + + context.put(repeat.var(), elementType); + context.put(repeat.pageVar(), page); + context.put("index", int.class); + context.put("isLast", boolean.class); + + } catch (ExpressionCompileException e) { + errors.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(e) + ); + } + + return context; + } + + + + + private void checkFormFields(Element element) { + if (null == form) + return; + + Attribute action = form.attribute("action"); + + // Only look at contextual uris (i.e. hosted by us). + if (null == action || (!action.getValue().startsWith("/"))) + return; + + final PageBook.Page page = pageBook.get(action.getValue()); + + // Only look at pages we actually have registered. + if (null == page) { + warnings.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(CompileErrors.UNRESOLVABLE_FORM_ACTION) + ); + + return; + } + + // If we're inside a form do a throw-away compile against the target page. + if ("input".equals(element.getName()) || "textarea".equals(element.getName())) { + Attribute name = element.attribute("name"); + + // Skip submits and buttons. + if (Dom.skippable(element.attribute("type"))) + return; + + //TODO Skip empty? + if (null == name) { + warnings.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(CompileErrors.FORM_MISSING_NAME) + ); + + return; + } + + // Compile expression path. + final String expression = name.getValue(); + try { + new MvelEvaluatorCompiler(page.pageClass()) + .compile(expression); + + } catch (ExpressionCompileException e) { + //TODO Very hacky, needed to strip out xmlns attribution. + warnings.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(CompileErrors.UNRESOLVABLE_FORM_BINDING, e) + ); + } + + } + + } + + private void checkUriConsistency(Element element) { + Attribute uriAttrib = element.attribute("action"); + if (null == uriAttrib) + uriAttrib = element.attribute("src"); + if (null == uriAttrib) + uriAttrib = element.attribute("href"); + + if (null != uriAttrib) { + + // Verify that such a uri exists in the page book, + // only if it is contextual--ignore abs & relative URIs. + final String uri = uriAttrib.getValue(); + if (uri.startsWith("/")) + if (null == pageBook.nonCompilingGet(uri)) + warnings.add( + CompileError.in(Dom.asRawXml(element)) + .near(Dom.lineNumberOf(element)) + .causedBy(CompileErrors.UNRESOLVABLE_FORM_ACTION, uri) + ); + } + } + + +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/MvelTemplateCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/MvelTemplateCompiler.java new file mode 100644 index 00000000..86c853b1 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/MvelTemplateCompiler.java @@ -0,0 +1,39 @@ +package com.google.sitebricks.compiler.template; + +import com.google.common.collect.ImmutableSet; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import org.mvel2.templates.CompiledTemplate; +import org.mvel2.templates.TemplateCompiler; +import org.mvel2.templates.TemplateRuntime; + +import java.util.Set; + +/** + * Creates renderables, given an MVEL template page. + */ +public class MvelTemplateCompiler { + private final Class page; + + public MvelTemplateCompiler(Class page) { + this.page = page; + } + + public Renderable compile(String template) { + // Compile template immediately. + final CompiledTemplate compiledTemplate = TemplateCompiler.compileTemplate(template); + + return new Renderable() { + @Override + public void render(Object bound, Respond respond) { + assert page.isInstance(bound); + respond.write(TemplateRuntime.execute(compiledTemplate, bound).toString()); + } + + @Override + public Set collect(Class clazz) { + return ImmutableSet.of(); + } + }; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/freemarker/FreemarkerTemplateCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/freemarker/FreemarkerTemplateCompiler.java new file mode 100644 index 00000000..d3e3dc24 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/freemarker/FreemarkerTemplateCompiler.java @@ -0,0 +1,78 @@ +package com.google.sitebricks.compiler.template.freemarker; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; + +import freemarker.core.Environment; +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; + +/** + * Creates renderables, given a Freemarker template page. + */ +public class FreemarkerTemplateCompiler { + private final Class page; + + public FreemarkerTemplateCompiler(Class page) { + this.page = page; + } + + public Renderable compile(String templateContent) { + + final Template template = getTemplate(page, templateContent); + + return new Renderable() { + @Override + public void render(Object bound, Respond respond) { + assert page.isInstance(bound); + Writer writer = new StringWriter(); + try { + template.process(bound, writer); + } + catch (TemplateException e) { + throw new RuntimeException(e); + } + catch (IOException e) { + throw new RuntimeException(e); + } + respond.write(writer.toString()); + } + + @Override + public Set collect(Class clazz) { + return ImmutableSet.of(); + } + }; + } + + private Template getTemplate(Class page, String content) + { + Configuration configuration = new Configuration(); + configuration.setTemplateExceptionHandler( new SitebricksTemplateExceptionHandler() ); + + try { + return new Template(page.getName(), new StringReader(content), configuration); + } + catch ( IOException e ) { + throw new RuntimeException( e ); + } + } + + class SitebricksTemplateExceptionHandler implements TemplateExceptionHandler { + public void handleTemplateException(TemplateException te, Environment env, Writer out) + throws TemplateException { + // We intentionally do nothing here + } + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/core/CaseWidget.java b/sitebricks/src/main/java/com/google/sitebricks/core/CaseWidget.java new file mode 100644 index 00000000..a449adbf --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/core/CaseWidget.java @@ -0,0 +1,20 @@ +package com.google.sitebricks.core; + +import com.google.sitebricks.rendering.With; +import com.google.sitebricks.rendering.EmbedAs; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@EmbedAs("Case") @With("When") +public class CaseWidget { + private Object choice; + + public Object getChoice() { + return choice; + } + + public void setChoice(Object choice) { + this.choice = choice; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/core/Repeat.java b/sitebricks/src/main/java/com/google/sitebricks/core/Repeat.java new file mode 100644 index 00000000..4d9008d5 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/core/Repeat.java @@ -0,0 +1,76 @@ +package com.google.sitebricks.core; + +import java.util.Collection; + +/** + *

+ * Repeats tagged content over a collection. This widget is the equivalent of a + * closure being projected across the given collection. Where the closure is around + * the content of the annotated tag (and any nested tags). Use attribute {@code var} + * to specify the name of the variable each collection entry will be bound to. + * For example, to create a list of movie titles: + *

+ * + *
+ * <ul>
+ *   {@code @Repeat}(items=movies, var="movie")
+ *   <li>${movie.title} starring ${movie.star}</li>
+ * </ul>
+ * 
+ * + *

+ * This dynamically produces a repetition of {@code <li>} tags containing the movie title + * and star for each entry in a collection property {@code movies}. Repeat widgets only + * work on items in a {@code java.util.Collection} (or subtype like {@code java.util.List}). + * It does *not* take arrays. + *

+ * + *

+ * Since the items attribute takes the result of an expression, you can place any expression in + * it that evaluates to a collection: + *

+ * + *
+ * <ul>
+ *   {@code @Repeat}(items=person.siblings, var="sib")
+ *   <li>${sib.name}</li>
+ * </ul>
+ * 
+ * + *

+ * This bit of code reads the siblings of a property {@code person} and binds it to a temporary + * variable {@code sib} that when repeating the list elements. + * + * Of course, the repeat widget repeats *any* content inside it, so you are free to nest any + * content you like inside, and they will be projected over the given collection: + *

+ * + *
+ * <div>
+ *   {@code @Repeat}(items=person.siblings, var="sib")
+ *   <div>
+ *      {@code @ShowIf}(sib.age > 1)
+ *      <div>
+ *          <p>${sib.name} is <b>${sib.age}</b> years old.</p>
+ *      </div>
+ *  </div>
+ * </div>
+ * 
+ * + * + *

+ * + *

+ * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@interface Repeat { + /** + * @instanceof java.util.Collection + * @return Returns any subclass of collection to project across. + */ + Class items(); //not really a Class + + String var() default "__this"; + String pageVar() default "__page"; +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/core/ShowIf.java b/sitebricks/src/main/java/com/google/sitebricks/core/ShowIf.java new file mode 100644 index 00000000..8d4d1bbc --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/core/ShowIf.java @@ -0,0 +1,39 @@ +package com.google.sitebricks.core; + + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + *

+ * Hides or shows content. Place this annotation on any tag to control whether it [and + * all children] are hidden or shown. {@code @ShowIf} takes a single direct argument of type + * boolean. You must provide this argument as an MVEL expression. For example: + *

+ * + *
+ * {@code @ShowIf}(true)
+ * <p>Hello World!</p>
+ * 
+ * + *

+ * Renders the content in the paragraph tags (including the tags themselves), and any + * nested content. Use the {@code @ShowIf(..)} expression to conditionally render parts of your template, + * controlled by logic from the page object: + *

+ * + *
+ * {@code @ShowIf}(movie.length > 2)
+ * <div>This movie is really looooong...</div>
+ * 
+ * + *

+ * You can, of course, annotate *any* HTML/XML element with {@code @ShowIf}. + *

+ * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Retention(RetentionPolicy.RUNTIME) +@interface ShowIf { + boolean value(); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/core/package-info.java b/sitebricks/src/main/java/com/google/sitebricks/core/package-info.java new file mode 100644 index 00000000..ce6abc52 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/core/package-info.java @@ -0,0 +1 @@ +package com.google.sitebricks.core; diff --git a/sitebricks/src/main/java/com/google/sitebricks/debug/DebugPage.html b/sitebricks/src/main/java/com/google/sitebricks/debug/DebugPage.html new file mode 100644 index 00000000..516411f3 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/debug/DebugPage.html @@ -0,0 +1,33 @@ + + + + Sitebricks Debug Console + + + + +

Web Pages

+ @Repeat(items=pages, var="page") +
+ ${page.uri} : ${page.pageClass().getName()} ${page.method.empty ? "" : page.method} +
+ +

Web Services

+ @Repeat(items=resources, var="page") +
+ ${page.uri} : ${page.pageClass().getName()} ${page.method.empty ? "" : page.method} +
+ + + \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/debug/DebugPage.java b/sitebricks/src/main/java/com/google/sitebricks/debug/DebugPage.java new file mode 100644 index 00000000..3ba00436 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/debug/DebugPage.java @@ -0,0 +1,53 @@ +package com.google.sitebricks.debug; + +import com.google.common.collect.Lists; +import com.google.inject.Inject; +import com.google.inject.servlet.RequestScoped; +import com.google.sitebricks.At; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.routing.PageBook; +import com.google.sitebricks.routing.PageBook.Page; + +import java.util.Collections; +import java.util.List; + +/** + * Page showing some stats about current sitebricks configuration. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@At("/debug") @RequestScoped +public class DebugPage { + @Inject + private PageBook pageBook; + + private List resources; + private List pages; + + @Get + void debug() { + resources = Lists.newArrayList(); + pages = Lists.newArrayList(); + for (List pages : pageBook.getPageMap()) { + for (Page page : pages) { + if (page.isHeadless()) { + resources.add(page); + } else { + this.pages.add(page); + } + } + } + + // O(n log n) + Collections.sort(resources); + Collections.sort(pages); + } + + public List getResources() { + return resources; + } + + public List getPages() { + return pages; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/headless/HeadlessRenderer.java b/sitebricks/src/main/java/com/google/sitebricks/headless/HeadlessRenderer.java new file mode 100644 index 00000000..a46b45f5 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/headless/HeadlessRenderer.java @@ -0,0 +1,16 @@ +package com.google.sitebricks.headless; + +import com.google.inject.ImplementedBy; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A utility that populates a servlet response with the data from a headless + * web service reply (typically a Reply object returned from a @Get (or + * similar) HTTP method on a @Service annotated class. + */ +@ImplementedBy(ReplyBasedHeadlessRenderer.class) +public interface HeadlessRenderer { + void render(HttpServletResponse response, Object o) throws IOException; +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/headless/Reply.java b/sitebricks/src/main/java/com/google/sitebricks/headless/Reply.java new file mode 100644 index 00000000..c7cb40d6 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/headless/Reply.java @@ -0,0 +1,136 @@ +package com.google.sitebricks.headless; + +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.sitebricks.client.Transport; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Map; + +/** + * Reply to a (headless) web request. + */ +public abstract class Reply { + // Asks sitebricks to continue down the servlet processing chain + public static final Reply NO_REPLY = Reply.saying(); + + public static final String NO_REPLY_ATTR = "sb_no_reply"; + + /** + * Perform a 301 redirect (moved permanently) to the given uri. + */ + public abstract Reply seeOther(String uri); + + /** + * Perform a custom redirect to the given uri. The status code must be + * in the 3XX range. + */ + public abstract Reply seeOther(String uri, int statusCode); + + /** + * The media type of the response data to send to the client. I.e. + * the mime-type. Example {@code "application/json"} for JSON responses. + */ + public abstract Reply type(String mediaType); + + /** + * A Map of headers to set directly on the response. + */ + public abstract Reply headers(Map headers); + + /** + * Perform a 404 not found reply. + */ + public abstract Reply notFound(); + + /** + * Perform a 401 not authorized reply. + */ + public abstract Reply unauthorized(); + + /** + * Directs sitebricks to use the given Guice key as a transport to + * marshall the provided entity to the client. Example: + *
+   *   return Reply.with(new Person(..)).as(Xml.class);
+   * 
+ *

+ * Will marhall the given Person object into XML using the Guice Key + * bound to [Xml.class] (by default this is an XStream based XML + * transport). + */ + public abstract Reply as(Class transport); + + /** + * Same as {@link #as(Class)}. + */ + public abstract Reply as(Key transport); + + /** + * Perform a 302 redirect to the given uri (moved temporarily). + */ + public abstract Reply redirect(String uri); + + /** + * Perform a 403 resource forbidden error response. + */ + public abstract Reply forbidden(); + + /** + * Perform a 204 no content response. + */ + public abstract Reply noContent(); + + /** + * Perform a 500 general error response. + */ + public abstract Reply error(); + + /** + * Render template associated with the given class. The class must have + * an @Show() annotation pointing to a valid Sitebricks template type (can + * be any of the supported templates: MVEL, freemarker, SB, etc.) + *

+ * The entity passed into with() is used as the template's context during + * render. + */ + public abstract Reply template(Class templateKey); + + /** + * Set a custom status code (call this last, it will be overridden if + * other response code directives are called afterward). + */ + public abstract Reply status(int code); + + /** + * Perform a 200 OK response with no body. + */ + public abstract Reply ok(); + + /** + * Used internally by sitebricks. Do NOT call. + */ + abstract void populate(Injector injector, HttpServletResponse response) throws IOException; + + /** + * Convenience method to make a reply without any entity or body. Example, to send a redirect: + *

+   *   return Reply.saying().redirect("/other");
+   * 
+ */ + public static Reply saying() { + return new ReplyMaker(null); + } + + /** + * Returns a reply with an entity that is sent back to the client via the specified + * transport. + * + * @param entity An entity to send back for which a valid transport exists (see + * {@link #as(Class)}). + */ + public static Reply with(E entity) { + return new ReplyMaker(entity); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/headless/ReplyBasedHeadlessRenderer.java b/sitebricks/src/main/java/com/google/sitebricks/headless/ReplyBasedHeadlessRenderer.java new file mode 100644 index 00000000..507dfc6b --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/headless/ReplyBasedHeadlessRenderer.java @@ -0,0 +1,34 @@ +package com.google.sitebricks.headless; + +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Singleton; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A renderer for pages that have no corresponding template, i.e. for headless + * web services using the Reply.with() API. + */ +@Singleton +class ReplyBasedHeadlessRenderer implements HeadlessRenderer { + private final Injector injector; + + @Inject + public ReplyBasedHeadlessRenderer(Injector injector) { + this.injector = injector; + } + + public void render(HttpServletResponse response, Object o) throws IOException { + if (null == o) { + throw new RuntimeException("Sitebricks received a null reply from the resource."); + } + + // Guaranteed by Sitebrick's page validator. + assert o instanceof Reply : o.getClass(); + Reply reply = (Reply)o; + + reply.populate(injector, response); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/headless/ReplyMaker.java b/sitebricks/src/main/java/com/google/sitebricks/headless/ReplyMaker.java new file mode 100644 index 00000000..35ef62a9 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/headless/ReplyMaker.java @@ -0,0 +1,196 @@ +package com.google.sitebricks.headless; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.sitebricks.client.Transport; +import com.google.sitebricks.client.transport.Text; +import com.google.sitebricks.rendering.Strings; +import com.google.sitebricks.rendering.Templates; +import org.apache.commons.io.IOUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +/** + * A builder implementation of the Reply interface. + */ +class ReplyMaker extends Reply { + + // By default, we cool. + private int status = HttpServletResponse.SC_OK; + + private String contentType; + + private String redirectUri; + private Map headers = Maps.newHashMap(); + + private Key transport = Key.get(Text.class); + private E entity; + private Class templateKey; + + public ReplyMaker(E entity) { + this.entity = entity; + } + + @Override + public Reply seeOther(String uri) { + redirectUri = uri; + status = HttpServletResponse.SC_MOVED_PERMANENTLY; + return this; + } + + @Override + public Reply seeOther(String uri, int statusCode) { + Preconditions.checkArgument(statusCode >= 300 && statusCode < 400, + "Redirect statuses must be between 300-399"); + redirectUri = uri; + status = statusCode; + return this; + } + + @Override + public Reply type(String mediaType) { + Strings.nonEmpty(mediaType, "Media type cannot be null or empty"); + this.contentType = mediaType; + return this; + } + + @Override + public Reply headers(Map headers) { + this.headers.putAll(headers); + return this; + } + + @Override + public Reply notFound() { + status = HttpServletResponse.SC_NOT_FOUND; + return this; + } + + @Override + public Reply unauthorized() { + status = HttpServletResponse.SC_UNAUTHORIZED; + return this; + } + + @Override + public Reply as(Key transport) { + Preconditions.checkArgument(null != transport, "Transport class cannot be null!"); + this.transport = transport; + return this; + } + + @Override + public Reply as(Class transport) { + Preconditions.checkArgument(null != transport, "Transport class cannot be null!"); + this.transport = Key.get(transport); + return this; + } + + @Override + public Reply redirect(String url) { + Strings.nonEmpty(url, "Redirect URL must be non empty!"); + this.redirectUri = url; + status = HttpServletResponse.SC_MOVED_TEMPORARILY; + return this; + } + + @Override + public Reply forbidden() { + status = HttpServletResponse.SC_FORBIDDEN; + return this; + } + + @Override + public Reply noContent() { + status = HttpServletResponse.SC_NO_CONTENT; + return this; + } + + @Override + public Reply error() { + status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + return this; + } + + @Override + public Reply status(int code) { + status = code; + return this; + } + + @Override + public Reply ok() { + status = HttpServletResponse.SC_OK; + return this; + } + + @Override + public Reply template(Class templateKey) { + this.templateKey = templateKey; + return this; + } + + @Override + void populate(Injector injector, HttpServletResponse response) throws IOException { + // If we should not bother with the chain + if (Reply.NO_REPLY == this) { + injector.getInstance(HttpServletRequest.class).setAttribute(Reply.NO_REPLY_ATTR, Boolean.TRUE); + return; + } + + // This is where we take all the builder values and encode them in the response. + Transport transport = injector.getInstance(this.transport); + + // Set any headers (we do this first, so we can override any cheekily set headers). + if (!headers.isEmpty()) { + for (Map.Entry header : headers.entrySet()) { + response.setHeader(header.getKey(), header.getValue()); + } + } + + // If the content type was already set, do nothing. + if (response.getContentType() == null) { + // By default we use the content type of the transport. + if (null == contentType) { + response.setContentType(transport.contentType()); + } else { + response.setContentType(contentType); + } + } + + + // Send redirect + if (null != redirectUri) { + response.sendRedirect(redirectUri); + response.setStatus(status); // HACK to override whatever status the redirect sets. + return; + } + + // Write out data. + response.setStatus(status); + + if (null != templateKey) { + response.getWriter().write(injector.getInstance(Templates.class).render(templateKey, entity)); + } else if (null != entity) { + if (entity instanceof InputStream) { + // Stream the response rather than marshalling it through a transport. + InputStream inputStream = (InputStream) entity; + try { + IOUtils.copy(inputStream, response.getOutputStream()); + } finally { + inputStream.close(); + } + } else { + // TODO(dhanji): This feels wrong to me. We need a better way to obtain the entity type. + transport.out(response.getOutputStream(), (Class) entity.getClass(), entity); + } + } + } +} + diff --git a/sitebricks/src/main/java/com/google/sitebricks/headless/Request.java b/sitebricks/src/main/java/com/google/sitebricks/headless/Request.java new file mode 100644 index 00000000..bf6069cb --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/headless/Request.java @@ -0,0 +1,79 @@ +package com.google.sitebricks.headless; + +import com.google.common.collect.Multimap; +import com.google.sitebricks.client.Transport; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Sitebricks abstraction of a request. May be a standard HTTP request, a tunneled + * Sitebricks RPC-over-HTTP, or another abstraction entirely. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public interface Request { + + /** + * Reads the raw request data into an object of the given type. Must + * be followed by a transport clause for correct unmarshalling. Example: + *
+   *   Person p = request.read(Person.class).as(Json.class);
+   * 
+ * + * @param type The target type to unmarshall the raw request data into. + * @return an instance containing the deserialized raw data. + */ + RequestRead read(Class type); + + /** + * Reads the request data directly into the given output stream. Useful + * for streaming uploads to a file or passthrough socket. + * + * @param out Any valid, open outputstream. Not closed after writing. + * + * @throws IOException If an error occurs during the streaming. + */ + void readTo(OutputStream out) throws IOException; + + /** + * Returns request headers as a multimap (to account for repeated headers). + */ + Multimap headers(); + + /** + * Returns request parameters as a multimap (to account for repeated values). + */ + Multimap params(); + + /** + * Returns matrix parameters as a multimap (to account for repeated values). + */ + Multimap matrix(); + + /** + * Returns the only value of a matrix parameter or null if the parameter + * was not present. + */ + String matrixParam(String name); + + /** + * Returns the only value of a request parameter or null if the parameter + * was not present. + *

+ * Behaves exactly like {@link javax.servlet.http.HttpServletRequest#getParameter(String)}. + */ + String param(String name); + + /** + * Returns the only value of a request header or null if the header + * was not present. + *

+ * Behaves exactly like {@link javax.servlet.http.HttpServletRequest#getHeader(String)}. + */ + String header(String name); + + public static interface RequestRead { + E as(Class transport); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/headless/Service.java b/sitebricks/src/main/java/com/google/sitebricks/headless/Service.java new file mode 100644 index 00000000..dad4e77b --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/headless/Service.java @@ -0,0 +1,49 @@ +package com.google.sitebricks.headless; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Analogous to {@literal @}Show but representing a headless (page-less) + * web service. Sometimes called a Restful web service. This kind of + * web page has no corresponding template file and the page class returns + * a response directly. + *

+ * By default, the service annotation will execute for any valid method + * bound at the specified URL (with the @At annotation or {@code at()} + * module method). + *

+ * However, it is possible to selectively dispatch services by specifying a + * value: + *

+ *
+ *  {@literal @}At("/doc"){@literal @}Service("fetch")
+ *   public class FetchDoc { .. }
+ * 
+ * + * In this scenario, all requests to "/doc" will be tested for the special + * request parameter "r". If "r" contains "fetch", then {@code FetchDoc} will + * execute, otherwise it will be ignored. + *

+ * The special request parameter "r" is a comma-separated list of service endpoints + * that a browser-client wishes to dispatch. This enables several convenient + * programming models: + *

    + *
  • RPC tunneling over HTTP
  • + *
  • Batching multiple operations over a single HTTP request
  • + *
  • Request fan-out (you can more easily service a single request + * from various backends)
  • + *
  • More responsive AJAX UIs
  • + *
+ *

+ * The special request parameter "r" can be customized via your {@code SitebricksModule} + * to be anything you like. If no endpoints match the request (i.e. "r=" is empty or + * corrupt), then a 405 (Method Not Allowed) error is returned. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Service { + String value() default ""; +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/Delete.java b/sitebricks/src/main/java/com/google/sitebricks/http/Delete.java new file mode 100644 index 00000000..f2d4245e --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/Delete.java @@ -0,0 +1,16 @@ +package com.google.sitebricks.http; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Delete { + String value() default ""; +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/Get.java b/sitebricks/src/main/java/com/google/sitebricks/http/Get.java new file mode 100644 index 00000000..1481751e --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/Get.java @@ -0,0 +1,15 @@ +package com.google.sitebricks.http; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Get { + String value() default ""; +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/Head.java b/sitebricks/src/main/java/com/google/sitebricks/http/Head.java new file mode 100644 index 00000000..206fbdf2 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/Head.java @@ -0,0 +1,15 @@ +package com.google.sitebricks.http; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Head { + String value() default ""; +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/Post.java b/sitebricks/src/main/java/com/google/sitebricks/http/Post.java new file mode 100644 index 00000000..188be314 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/Post.java @@ -0,0 +1,16 @@ +package com.google.sitebricks.http; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Post { + public abstract String value() default ""; +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/Put.java b/sitebricks/src/main/java/com/google/sitebricks/http/Put.java new file mode 100644 index 00000000..77a425ab --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/Put.java @@ -0,0 +1,16 @@ +package com.google.sitebricks.http; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Put { + String value() default ""; +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/Select.java b/sitebricks/src/main/java/com/google/sitebricks/http/Select.java new file mode 100644 index 00000000..0282de2b --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/Select.java @@ -0,0 +1,34 @@ +package com.google.sitebricks.http; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This annotation is used to select request handlers based on + * request parameters. For example, in a single resource URL, you + * may wish to call different handlers for POST based on the request + * parameter "action" (action=update, action=delete, etc.). These + * maybe modeled as form parameters or as part of the query string. + * + *

+ *   {@literal @}At("/city/atlantis") {@literal @} Select("action")
+ *   public class PictureWebService {
+ *
+ *     {@literal @}Post("update")
+ *     public void update() {
+ *       // edit resource in place
+ *     }
+ *
+ *     {@literal @}Post("delete")
+ *     public void delete() {
+ *       // remove the item...
+ *     }
+ *   }
+ * 
+ * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Select { + String value(); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/Trace.java b/sitebricks/src/main/java/com/google/sitebricks/http/Trace.java new file mode 100644 index 00000000..aa1d6378 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/Trace.java @@ -0,0 +1,15 @@ +package com.google.sitebricks.http; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Trace { + String value() default ""; +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/Accept.java b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/Accept.java new file mode 100644 index 00000000..9b430e75 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/Accept.java @@ -0,0 +1,40 @@ +package com.google.sitebricks.http.negotiate; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used to select request handlers based on + * request headers provided by clients and can be used to perform HTTP + * content negotiation. If a client sends in a request for an + * image via uri, "/city/atlantis" and accepts JPEG type, then + * you may instruct sitebricks to choose a request handler as + * follows: + *
+ *   {@literal @}At("/city/atlantis")
+ *   public class PictureWebService {
+ *
+ *     {@literal @}Accept("Accept") @Get("image/jpeg")
+ *     public Response getJpeg() {
+ *       //return JPEG image...
+ *     }
+ *
+ *     {@literal @}Accept("Accept") @Get("image/png")
+ *     public Response getPng() {
+ *       //return PNG image instead...
+ *     }
+ *   }
+ * 
+ * + * + * Note that you cannot mix the two kinds of negotiation. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Accept { + public abstract String value(); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ConnegModule.java b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ConnegModule.java new file mode 100644 index 00000000..c34e3063 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ConnegModule.java @@ -0,0 +1,15 @@ +package com.google.sitebricks.http.negotiate; + +import com.google.sitebricks.SitebricksModule; + +/** + * Bindings for content negotiation defaults. + */ +public class ConnegModule extends SitebricksModule { + + @Override + protected void configureSitebricks() { + // NOTE(dhanji): Unused at the moment. + negotiate("Accept").with(Accept.class); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ContentNegotiator.java b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ContentNegotiator.java new file mode 100644 index 00000000..d31c3fc0 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ContentNegotiator.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.http.negotiate; + +import com.google.inject.ImplementedBy; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * The strategy for deciding if a Java request handler method, annotated with content + * negotiation metadata should fire against the given http request. + * + * The default strategy compares the value of the annotation to the value(s) of the header + * exactly. + */ +@ImplementedBy(ExactMatchNegotiator.class) +public interface ContentNegotiator { + + /** + * Tests whether a given http request (and its headers) should pass against the given + * map of content negotiation rules. + * + * @param negotiations A Map of header names to match expressions (the value part of an + * annotation). For example, an annotation {@literal @}Accept("text/html") would produce + * a map entry of ["Accept" -> "text/html"], assuming that the annotation is mapped via + * the {@linkplain com.google.sitebricks.SitebricksModule#negotiate} method to the "Accept" + * http header. + * @param request The current http request to match against. + * @return True if the negotiation succeeded on this method. + */ + boolean shouldCall(Map negotiations, HttpServletRequest request); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ExactMatchNegotiator.java b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ExactMatchNegotiator.java new file mode 100644 index 00000000..1108569a --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/ExactMatchNegotiator.java @@ -0,0 +1,37 @@ +package com.google.sitebricks.http.negotiate; + +import com.google.common.collect.Iterables; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.Map; + +/** + * A strategy for deciding whether or not a header is acceptable to the given + * method map header expressions. This strategy literally matches the value in + * a header annotation to the value of the given header, and is case sensitive. + */ +class ExactMatchNegotiator implements ContentNegotiator { + public boolean shouldCall(Map negotiations, HttpServletRequest request) { + for (Map.Entry negotiate : negotiations.entrySet()) { + + @SuppressWarnings("unchecked") // Guaranteed by servlet spec. + Enumeration headerValues = request.getHeaders(negotiate.getKey()); + + // Guaranteed never to throw NPE. + boolean shouldFire = false; + while(headerValues.hasMoreElements()) { + String value = headerValues.nextElement(); + + // Everything has to pass for us to say OK. + shouldFire |= Iterables.contains(Arrays.asList(value.split(",[ ]*")), negotiate.getValue()); + } + if (!shouldFire) { + return false; + } + } + + return true; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/Negotiation.java b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/Negotiation.java new file mode 100644 index 00000000..c79a1d87 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/Negotiation.java @@ -0,0 +1,16 @@ +package com.google.sitebricks.http.negotiate; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A simple binding annotation to share negotiation services. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@Retention(RetentionPolicy.RUNTIME) +@BindingAnnotation +public @interface Negotiation { +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/RegexNegotiator.java b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/RegexNegotiator.java new file mode 100755 index 00000000..6424154f --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/RegexNegotiator.java @@ -0,0 +1,38 @@ +package com.google.sitebricks.http.negotiate; + +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; +import java.util.Map; + +/** + * ContentNegotiator that supports one regex match value, for example + * {@literal @}Accept("(xml|text)/.*") will match any incoming request with an + * HTTP Accept header "text/*" and {@literal @}Referer("(google|yahoo|bing)\\.com") will + * match requests with HTTP Referer headers from google, yahoo, or bing + */ +public class RegexNegotiator implements ContentNegotiator { + + public boolean shouldCall(Map negotiations, HttpServletRequest request) { + for (Map.Entry negotiate : negotiations.entrySet()) { + + @SuppressWarnings("unchecked") // Guaranteed by servlet spec. + Enumeration headerValues = request.getHeaders(negotiate.getKey()); + String match = negotiate.getValue(); + + boolean shouldFire = false; // Guaranteed never to throw NPE + while (headerValues.hasMoreElements()) { + String value = headerValues.nextElement(); + + shouldFire |= value.matches(match); + + for (String val: value.split(",[ ]*")) { + shouldFire |= val.matches(match); + } + } + if (!shouldFire) { + return false; + } + } + return true; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/WildcardNegotiator.java b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/WildcardNegotiator.java new file mode 100755 index 00000000..4e8d8d42 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/http/negotiate/WildcardNegotiator.java @@ -0,0 +1,100 @@ +package com.google.sitebricks.http.negotiate; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Sets; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * ContentNegotiator that supports comma separated and wildcard matches in Accept header style + * for example, {@literal @}Accept("text/html, text/plain") will match an incoming request + * with HTTP Accept header "text/*" and {@literal @}Accept("text/*") will match incoming + * request with headers "Accept: text/html" or "Accept: text/plain" + * + * Notes: + * Wildcard for subtypes such as "text/*, image/*" are supported but there is no + * wildcard matching on the main media types. Negotiating on other HTTP request + * headers where "/*" might be useful is currently undefined. + * + * + */ +public class WildcardNegotiator implements ContentNegotiator { + // Lifted TOKEN, TYPE_PATTERN from com.google.gdata.util + + private static String TOKEN = + "[\\p{ASCII}&&[^\\p{Cntrl} ;/=\\[\\]\\(\\)\\<\\>\\@\\,\\:\\\"\\?\\=]]+"; + + private static Pattern TYPE_PATTERN = Pattern.compile( + "(" + TOKEN + ")" + // mediatype (G1) + "/" + // separator + "(" + TOKEN + ")" + // subtype (G2) + "\\s*(.*)\\s*", Pattern.DOTALL); + + private HashMultimap createMultimatch(List matchlist) { + HashMultimap multimatch = HashMultimap.create(); + for (String m : matchlist) { + Matcher mediaType = TYPE_PATTERN.matcher(m); + + if (mediaType.matches()) { + String type = mediaType.group(1).toLowerCase(); + String subtype = mediaType.group(2).toLowerCase(); + multimatch.put(type, subtype); + } + } + return multimatch; + } + + public boolean shouldCall(Map negotiations, HttpServletRequest request) { + for (Map.Entry negotiate : negotiations.entrySet()) { + @SuppressWarnings("unchecked") // Guaranteed by servlet spec. + Enumeration headerValues = request.getHeaders(negotiate.getKey()); + + boolean shouldFire = false; + + List matches = Arrays.asList(negotiate.getValue().split(",[ ]*")); + HashMultimap mediaMatches = createMultimatch(matches); + + while (headerValues.hasMoreElements()) { + String value = headerValues.nextElement(); + + List values = Arrays.asList(value.split(",[ ]*")); + HashMultimap mediaValues = createMultimatch(values); + + if (!mediaMatches.isEmpty()) { + Set typeIntersection = Sets.intersection(mediaMatches.keySet(), mediaValues.keySet()); + if (typeIntersection.isEmpty()) { + shouldFire |= typeIntersection.isEmpty(); + } else { + for (String mediaType: typeIntersection) { + Set subtypeMatches = mediaMatches.get(mediaType); + Set subtypeValues = mediaValues.get(mediaType); + + shouldFire |= (subtypeMatches.contains("*") + || subtypeValues.contains("*") + || !Sets.intersection(subtypeMatches, subtypeValues).isEmpty()); + } + } + } else { + shouldFire |= !(Collections.disjoint(Arrays.asList(value.split(",[ ]*")), matches)); + } + } + if (!shouldFire) { + return false; + } + } + return true; + } +} + +// TODO - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html +// Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5 + +// TODO - other headers with slashes (but not signifying media types) diff --git a/sitebricks/src/main/java/com/google/sitebricks/i18n/Message.java b/sitebricks/src/main/java/com/google/sitebricks/i18n/Message.java new file mode 100644 index 00000000..ffed0454 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/i18n/Message.java @@ -0,0 +1,14 @@ +package com.google.sitebricks.i18n; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annotate localization interface methods with this to describe the purpose and value of + * an internationalized message. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Message { + String message(); + String description() default ""; +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/Attributes.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/Attributes.java new file mode 100644 index 00000000..0cbca9ad --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/Attributes.java @@ -0,0 +1,17 @@ +package com.google.sitebricks.rendering; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER, ElementType.FIELD}) +@BindingAnnotation //not really +public @interface Attributes { +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/Decorated.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/Decorated.java new file mode 100644 index 00000000..4ee96b18 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/Decorated.java @@ -0,0 +1,18 @@ +package com.google.sitebricks.rendering; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the page is to be rendered and its output inserted into + * a decorator page using the @Decorate template annotation. + * + * @author John Patterson (jdpatterson@gmail.com) + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Decorated { +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/EmbedAs.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/EmbedAs.java new file mode 100644 index 00000000..23f2f341 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/EmbedAs.java @@ -0,0 +1,16 @@ +package com.google.sitebricks.rendering; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface EmbedAs { + String value(); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/SelfRendering.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/SelfRendering.java new file mode 100644 index 00000000..b22ea70d --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/SelfRendering.java @@ -0,0 +1,14 @@ +package com.google.sitebricks.rendering; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface SelfRendering { +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/Strings.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/Strings.java new file mode 100644 index 00000000..21e2779b --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/Strings.java @@ -0,0 +1,43 @@ +package com.google.sitebricks.rendering; + +/** + * A string-specific set of utilities. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class Strings { + private Strings() { + } + + /** + * Tests for null or emptiness of a string, throwing an + * {@link IllegalArgumentException} if one is encountered. + * + * @param aString Any string to test for emptiness. + * @param message A message to throw inside an IllegalArgumentException if + * the {@code aString} was empty. + */ + public static void nonEmpty(String aString, String message) { + if (empty(aString)) + throw new IllegalArgumentException(message); + } + + /** + * @param string Any string to test for emptiness. + * @return True if this string is empty or null. + */ + public static boolean empty(String string) { + return null == string || "".equals(string.trim()); + } + + public static String join(String[] strings, char sep) { + StringBuilder builder = new StringBuilder(); + for (String string : strings) { + builder.append(string); + builder.append(sep); + } + builder.deleteCharAt(builder.length() - 1); + + return builder.toString(); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/Templates.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/Templates.java new file mode 100644 index 00000000..2441d9e7 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/Templates.java @@ -0,0 +1,85 @@ +package com.google.sitebricks.rendering; + +import com.google.common.base.Preconditions; +import com.google.common.collect.MapMaker; +import com.google.inject.Inject; +import com.google.inject.Stage; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.StringBuilderRespond; +import com.google.sitebricks.Template; +import com.google.sitebricks.TemplateLoader; +import com.google.sitebricks.compiler.Compilers; + +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class Templates { + private final TemplateLoader loader; + private final Compilers compilers; + private final boolean reloadTemplates; + + private final ConcurrentMap, Renderable> templates = new MapMaker().makeMap(); + + @Inject + public Templates(TemplateLoader loader, Compilers compilers, Stage stage) { + this.loader = loader; + this.compilers = compilers; + this.reloadTemplates = Stage.DEVELOPMENT == stage; + } + + public void loadAll(Set templates) { + // If in production mode, force load all the templates. + for (Descriptor template : templates) { + Renderable compiled = compilers.compile(template.clazz); + Preconditions.checkArgument(null != compiled, "No template found attached to: %s", + template.clazz); + + this.templates.put(template.clazz, compiled); + } + } + + public String render(Class clazz, Object context) { + Renderable compiled; + if (reloadTemplates) { + compiled = compilers.compile(clazz); + + templates.put(clazz, compiled); + } else { + compiled = templates.get(clazz); + } + Preconditions.checkArgument(null != compiled, "No template found attached to: %s", clazz); + + StringBuilderRespond respond = new StringBuilderRespond(); + //noinspection ConstantConditions + compiled.render(context, respond); + + return respond.toString(); + } + + public static class Descriptor { + private final Class clazz; + private final String fileName; + private final Template.Kind kind; + + public Descriptor(Class clazz, String fileName) { + this.clazz = clazz; + this.fileName = fileName; + this.kind = Template.Kind.kindOf(fileName); + } + + public Class getClazz() { + return clazz; + } + + public String getFileName() { + return fileName; + } + + public Template.Kind getKind() { + return kind; + } + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/With.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/With.java new file mode 100644 index 00000000..7b0b6115 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/With.java @@ -0,0 +1,16 @@ +package com.google.sitebricks.rendering; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface With { + String[] value(); +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ArgumentWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ArgumentWidget.java new file mode 100644 index 00000000..ac8fe71e --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ArgumentWidget.java @@ -0,0 +1,45 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.compiler.Parsing; +import com.google.sitebricks.rendering.SelfRendering; +import net.jcip.annotations.Immutable; + +import java.util.Set; + +/** + * Used to embed an argument inside an embedding widget (for later inclusion by the @Include widget). + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@SelfRendering @Immutable +class ArgumentWidget implements Renderable { + private final WidgetChain widgetChain; + private final String expression; + private final Evaluator evaluator; + + public ArgumentWidget(WidgetChain widgetChain, String expression, Evaluator evaluator) { + this.widgetChain = widgetChain; + this.expression = Parsing.stripQuotes(expression); + this.evaluator = evaluator; + } + + public void render(Object bound, Respond respond) { + widgetChain.render(bound, respond); + } + + public Set collect(Class clazz) { + return widgetChain.collect(clazz); + } + + /** + * + * @return Returns the embedded argument text. For example, in {@code @When("red")}, will return "red". + * + */ + public String getName() { + return expression; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/Chains.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/Chains.java new file mode 100644 index 00000000..34f85196 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/Chains.java @@ -0,0 +1,23 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Renderable; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public final class Chains { + private Chains() { + } + + public static WidgetChain terminal() { + return new TerminalWidgetChain(); + } + + public static WidgetChain singleton(Renderable widget) { + return new SingletonWidgetChain(widget); + } + + public static WidgetChain proceeding() { + return new ProceedingWidgetChain(); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ChooseWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ChooseWidget.java new file mode 100644 index 00000000..2613f3e2 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ChooseWidget.java @@ -0,0 +1,75 @@ +package com.google.sitebricks.rendering.control; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.binding.FlashCache; +import com.google.sitebricks.compiler.Parsing; +import com.google.sitebricks.rendering.SelfRendering; +import net.jcip.annotations.Immutable; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Immutable @SelfRendering +class ChooseWidget implements Renderable { + private final WidgetChain widgetChain; + private final Map map; + private final Evaluator evaluator; + + private volatile Provider cache; + + public ChooseWidget(WidgetChain widgetChain, String expression, Evaluator evaluator) { + this.evaluator = evaluator; + this.map = Parsing.toBindMap(expression); + this.widgetChain = widgetChain; + + //TODO validate expression + } + + public void render(Object bound, Respond respond) { + final String from = map.get("from"); + + Object o = evaluator.read(from, bound); + + if (!(o instanceof Collection)) + throw new IllegalArgumentException("@Choose widget's from argument MUST be of type java.util.Collection " + + "but was: " + (null == o ? "null" : o.getClass())); + + respond.write(""); + + //store for later retrieval during binding + cache.get().put(from, collection); + } + + public Set collect(Class clazz) { + return widgetChain.collect(clazz); + } + + @Inject + public void setCache(Provider cache) { + this.cache = cache; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/DecorateWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/DecorateWidget.java new file mode 100644 index 00000000..f8ca75f1 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/DecorateWidget.java @@ -0,0 +1,101 @@ +package com.google.sitebricks.rendering.control; + +import java.util.Collections; +import java.util.Set; + +import com.google.inject.Inject; +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.StringBuilderRespond; +import com.google.sitebricks.rendering.Decorated; +import com.google.sitebricks.routing.PageBook; + +/** + * @author John Patterson (jdpatterson@gmail.com) + * + */ +public class DecorateWidget implements Renderable { + + @Inject private PageBook book; + + private ThreadLocal> templateClassLocal = new ThreadLocal>(); + + public static String embedNameFor(Class pageClass) { + return pageClass.getName().toLowerCase() + "-extend"; + } + + public DecorateWidget(WidgetChain chain, String expression, Evaluator evaluator){ + // do not need any of the compulsory constructor args + } + + @Override + public void render(Object bound, Respond respond) { + + Class templateClass; + Class previousTemplateClass = templateClassLocal.get(); + try { + if (previousTemplateClass == null) { + templateClass = bound.getClass(); + } + else { + // get the extension subclass above the last + templateClass = nextExtensionSubclass(previousTemplateClass, bound.getClass()); + if (templateClass == null) { + throw new IllegalStateException("Could not find subclass of " + previousTemplateClass.getName() + " with @Extension annotation."); + } + } + templateClassLocal.set(templateClass); + + // get the extension page by name + PageBook.Page page = book.forName(DecorateWidget.embedNameFor(templateClass)); + + // create a dummy respond to collect the output of the embedded page + StringBuilderRespond sbrespond = new StringBuilderRespond(); + EmbeddedRespond embedded = new EmbeddedRespond(null, sbrespond); + page.widget().render(bound, embedded); + + // write the head and content to the real respond + respond.writeToHead(embedded.toHeadString()); + respond.write(embedded.toString()); + + // free some memory + embedded.clear(); + } + finally { + // we are finished with this extension + if (previousTemplateClass == null) { + templateClassLocal.set(null); + } + } + } + + // recursively find the next subclass with an @Extension annotation + private Class nextExtensionSubclass(Class previousTemplagteClass, Class candidate) { + if (candidate == previousTemplagteClass) { + // terminate the recursion + return null; + } + else if (candidate == Object.class) { + // this should never happen - we should terminate recursion first + throw new IllegalStateException("Did not find previsou extension"); + } + else { + // check the super class for the result + Class result = nextExtensionSubclass(previousTemplagteClass, candidate.getSuperclass()); + if (result == null && candidate.isAnnotationPresent(Decorated.class)) { + // this is the one - retreat! + return candidate; + } + else { + // we still have not found one + return null; + } + } + } + + @Override + public Set collect(Class clazz) { + return Collections.emptySet(); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/DefaultWidgetRegistry.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/DefaultWidgetRegistry.java new file mode 100644 index 00000000..2b708545 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/DefaultWidgetRegistry.java @@ -0,0 +1,164 @@ +package com.google.sitebricks.rendering.control; + +import com.google.common.collect.MapMaker; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Singleton; +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.Parsing; +import com.google.sitebricks.compiler.RepeatToken; +import com.google.sitebricks.routing.PageBook; +import net.jcip.annotations.ThreadSafe; + +import java.util.Map; +import java.util.concurrent.ConcurrentMap; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ThreadSafe +@Singleton +class DefaultWidgetRegistry implements WidgetRegistry { + public static final String TEXT_WIDGET = "__w:wRawText_Widget"; + + private final Injector injector; + private final Evaluator evaluator; + private final PageBook pageBook; + + private final ConcurrentMap widgets = new MapMaker().makeMap(); + + @Inject + public DefaultWidgetRegistry(Evaluator evaluator, PageBook pageBook, Injector injector) { + this.evaluator = evaluator; + this.pageBook = pageBook; + this.injector = injector; + + //register core sitebricks controllers. + addCoreControllers(); + } + + private void addCoreControllers() { + //TODO make these case sensitive + add("textfield", TextFieldWidget.class); + add("repeat", RepeatWidget.class); + add("showif", ShowIfWidget.class); + add("choose", ChooseWidget.class); + add("include", IncludeWidget.class); + add("decorated", DecorateWidget.class); + } + + public void add(String key, Class widget) { + widgets.put(key.toLowerCase().trim(), WidgetWrapper.forWidget(key, widget)); + } + + public boolean isSelfRendering(String widget) { + WidgetWrapper wrapper = widgets.get(widget); + + if (null == wrapper) { + throw new NoSuchWidgetException( + "No widget found matching the name: @" + + widget + " ; Did you forget to bind your" + + " widget class using the embed().as() rule?"); + } + + return wrapper.isSelfRendering(); + } + + + public RepeatToken parseRepeat(String expression) { + //parse and convert widget into metadata annotation + final Map bindMap = Parsing.toBindMap(expression); + + //noinspection OverlyComplexAnonymousInnerClass + return new RepeatToken() { + + public String items() { + return bindMap.get(RepeatToken.ITEMS); + } + + public String var() { + final String var = bindMap.get(RepeatToken.VAR); + + return null != var ? Parsing.stripQuotes(var) : null; + } + + public String pageVar() { + final String pageVar = bindMap.get(RepeatToken.PAGE_VAR); + + return null == pageVar ? RepeatToken.DEFAULT_PAGEVAR : pageVar; + } + }; + } + + public Renderable headWidget(WidgetChain childsChildren, + Map attribs, EvaluatorCompiler compiler) + throws ExpressionCompileException { + return new HeaderWidget(childsChildren, attribs, compiler); + } + + public XmlWidget xmlWidget(WidgetChain childsChildren, String elementName, + Map attribs, + EvaluatorCompiler compiler) throws ExpressionCompileException { + + final XmlWidget widget = new XmlWidget(childsChildren, elementName, compiler, attribs); + injector.injectMembers(widget); + + return widget; + } + + public Renderable newWidget(String key, String expression, WidgetChain widgetChain, + EvaluatorCompiler compiler) + throws ExpressionCompileException { + + if (!widgets.containsKey(key)) { + throw new NoSuchWidgetException("No such widget registered (did you add" + + " it correctly in module setup?): " + key); + } + + if (TEXT_WIDGET.equals(key)) + return new TextWidget(null, compiler); + + //otherwise construct via reflection (all sitebricks MUST have + // a constructor with: widgetchain, expression, evaluator; in that order) + final Renderable widget = widgets + .get(key) + .newWidget(widgetChain, expression, evaluator, pageBook); + + //add some injection (some sitebricks require it). It's a bit hacky, maybe we can reimplement some stuff later with @AssistedInject + injector.injectMembers(widget); + + return widget; + } + + public Renderable requireWidget(String template, EvaluatorCompiler compiler) + throws ExpressionCompileException { + return new RequireWidget(template, compiler); + } + + public Renderable textWidget(String template, EvaluatorCompiler compiler) + throws ExpressionCompileException { + return new TextWidget(template, compiler); + } + + public Renderable rawTextWidget(String template, EvaluatorCompiler compiler) + throws ExpressionCompileException { + return new RawTextWidget(template, compiler); + } + + @Override + public Renderable xmlDirectiveWidget(String wholeDeclaration, EvaluatorCompiler evaluatorCompiler) + throws ExpressionCompileException { + return new XmlDirectiveWidget(wholeDeclaration, evaluatorCompiler); + } + + public void addEmbed(String embedAs) { + add(embedAs, EmbedWidget.class); + } + + public void addArgument(String callWith) { + add(callWith, ArgumentWidget.class); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbedWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbedWidget.java new file mode 100644 index 00000000..bd990686 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbedWidget.java @@ -0,0 +1,78 @@ +package com.google.sitebricks.rendering.control; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.compiler.Parsing; +import com.google.sitebricks.routing.PageBook; +import net.jcip.annotations.Immutable; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Immutable +class EmbedWidget implements Renderable { + private final Map bindExpressions; + private final Map arguments; + private final Evaluator evaluator; + private final PageBook pageBook; + private final String targetPage; + + private EmbeddedRespondFactory factory; + private Provider request; + + public EmbedWidget(Map arguments, String expression, + Evaluator evaluator, PageBook pageBook, String targetPage) { + this.arguments = arguments; + + this.evaluator = evaluator; + this.pageBook = pageBook; + this.targetPage = targetPage.toLowerCase(); + + //parse expression list + this.bindExpressions = Parsing.toBindMap(expression); + } + + + public void render(Object bound, Respond respond) { + PageBook.Page page = pageBook.forName(targetPage); + + //create an instance of the embedded page + Object pageObject = page.instantiate(); + + //bind parameters to it as necessary + for (Map.Entry entry : bindExpressions.entrySet()) { + evaluator.write(entry.getKey(), pageObject, evaluator.evaluate(entry.getValue(), bound)); + } + + //chain to embedded page (widget), with arguments + EmbeddedRespond embed = factory.get(arguments); + + HttpServletRequest req = request.get(); + page.doMethod(req.getMethod(), pageObject, "", req); + page.widget().render(pageObject, embed); + + //extract and write embedded response to enclosing page's respond + respond.writeToHead(embed.toHeadString()); //TODO only write @Require tags + respond.write(embed.toString()); + + embed.clear(); + } + + public Set collect(Class clazz) { + return Collections.emptySet(); + } + + @Inject + public void init(EmbeddedRespondFactory factory, Provider request) { + this.factory = factory; + this.request = request; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespond.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespond.java new file mode 100644 index 00000000..a2f3b68a --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespond.java @@ -0,0 +1,132 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Respond; + +import java.util.Map; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +class EmbeddedRespond implements Respond { + private static final String BODY_BEGIN = " arguments; + private final Respond delegate; + + public EmbeddedRespond(Map arguments, Respond respond) { + this.arguments = arguments; + this.delegate = respond; + } + + public String toHeadString() { + if (null == body) { + //extract and store + extract(delegate.toString()); + } + + //we discard the tag rendered in the child page and + //instead return only what was directly rendered with writeToHead() + return delegate.getHead(); + } + + //state machine extracts tag content + + private void extract(String htmlDoc) { + + //now extract the contents of ... + int bodyStart = htmlDoc.indexOf(BODY_BEGIN) + BODY_BEGIN.length(); + + //scan for end of the start tag (beginning of body content) + char quote = NOT_IN_QUOTE; + for (int body = bodyStart; body < htmlDoc.length(); body++) { + final char c = htmlDoc.charAt(body); + if (isQuoteChar(c)) { + if (quote == NOT_IN_QUOTE) + quote = c; + else if (quote == c) + quote = NOT_IN_QUOTE; + } + + if ('>' == c && NOT_IN_QUOTE == quote) { + bodyStart = body + 1; + break; + } + } + + int bodyEnd = htmlDoc.indexOf(BODY_END, bodyStart); + + //if there was no body tag, just embed whatever was rendered directly + if (-1 == bodyEnd) { + EmbeddedRespond.this.body = htmlDoc; + } else + EmbeddedRespond.this.body = htmlDoc.substring(bodyStart, bodyEnd); + } + + + private static boolean isQuoteChar(char c) { + return '"' == c || '\'' == c; + } + + public void write(String text) { + delegate.write(text); + } + + public HtmlTagBuilder withHtml() { + return delegate.withHtml(); + } + + public void write(char c) { + delegate.write(c); + } + + public void require(String require) { + delegate.require(require); + } + + public void redirect(String to) { + delegate.redirect(to); + } + + public void writeToHead(String text) { + delegate.writeToHead(text); + } + + public void chew() { + delegate.chew(); + } + + public String getRedirect() { + return delegate.getRedirect(); + } + + + public String getContentType() { + return delegate.getContentType(); + } + + public ArgumentWidget include(String name) { + return arguments.get(name); + } + + public String getHead() { + return delegate.getHead(); + } + + public void clear() { + delegate.clear(); + } + + @Override + public String toString() { + if (null == body) { + extract(super.toString()); + } + + return body; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespondFactory.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespondFactory.java new file mode 100644 index 00000000..810c8d24 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespondFactory.java @@ -0,0 +1,24 @@ +package com.google.sitebricks.rendering.control; + +import com.google.inject.Inject; +import com.google.sitebricks.Respond; +import net.jcip.annotations.Immutable; + +import java.util.Map; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@Immutable +class EmbeddedRespondFactory { + private final Respond respond; + + @Inject + public EmbeddedRespondFactory(Respond respond) { + this.respond = respond; + } + + public EmbeddedRespond get(Map arguments) { + return new EmbeddedRespond(arguments, respond); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/HeaderWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/HeaderWidget.java new file mode 100644 index 00000000..df40fdb9 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/HeaderWidget.java @@ -0,0 +1,46 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.Token; +import com.google.sitebricks.rendering.SelfRendering; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@SelfRendering +class HeaderWidget implements Renderable { + private final WidgetChain widgetChain; + private Map> attribs; + + public HeaderWidget(WidgetChain widgetChain, Map attribs, + EvaluatorCompiler compiler) throws ExpressionCompileException { + + this.widgetChain = widgetChain; + this.attribs = XmlWidget.compile(attribs, compiler); + } + + public void render(Object bound, Respond respond) { + XmlWidget.writeOpenTag(bound, respond, "head", attribs); + + respond.write('>'); + + //render children (as necessary) + widgetChain.render(bound, respond); + + respond.withHtml() + .headerPlaceholder(); //TODO replace placeholder with an index? + respond.write(""); + } + + + public Set collect(Class clazz) { + return widgetChain.collect(clazz); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/IncludeWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/IncludeWidget.java new file mode 100644 index 00000000..e8efae43 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/IncludeWidget.java @@ -0,0 +1,30 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; + +import java.util.Collections; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +class IncludeWidget implements Renderable { + private final String name; + private final Evaluator evaluator; + + public IncludeWidget(WidgetChain chain, String name, Evaluator evaluator) { + this.name = name; + this.evaluator = evaluator; + } + + public void render(Object bound, Respond respond) { + respond.include((String) evaluator.evaluate(name, bound)) + .render(bound, respond); + } + + public Set collect(Class clazz) { + return Collections.emptySet(); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/NoSuchWidgetException.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/NoSuchWidgetException.java new file mode 100644 index 00000000..a08ebc27 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/NoSuchWidgetException.java @@ -0,0 +1,10 @@ +package com.google.sitebricks.rendering.control; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +class NoSuchWidgetException extends RuntimeException { + public NoSuchWidgetException(String msg) { + super(msg); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ProceedingWidgetChain.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ProceedingWidgetChain.java new file mode 100644 index 00000000..d7d1e597 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ProceedingWidgetChain.java @@ -0,0 +1,52 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import net.jcip.annotations.ThreadSafe; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ThreadSafe +class ProceedingWidgetChain implements WidgetChain { + private final List widgets = new ArrayList(); + + public void render(Object bound, Respond respond) { + for (Renderable widget : widgets) { + widget.render(bound, respond); + } + } + + public synchronized WidgetChain addWidget(Renderable renderable) { + widgets.add(renderable); + return this; + } + + /** + * This is an expensive method, never use it when live (used best at startup). + * + * @param clazz A class implementing {@code Renderable}. + * @return Returns a set of widgets that match the given type in this widget chain. + */ + public synchronized Set collect(Class clazz) { + Set matches = new HashSet(); + for (Renderable widget : widgets) { + + //add any matching classes to the set + if (clazz.isInstance(widget)) + //noinspection unchecked + matches.add((T) widget); + + //traverse down widget chains + if (widget instanceof WidgetChain) + matches.addAll(widget.collect(clazz)); + } + + return matches; + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/RawTextWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/RawTextWidget.java new file mode 100644 index 00000000..5539ed0e --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/RawTextWidget.java @@ -0,0 +1,28 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.rendering.SelfRendering; +import net.jcip.annotations.ThreadSafe; + +import java.util.Collections; +import java.util.Set; + +@ThreadSafe @SelfRendering +public class RawTextWidget implements Renderable { + private String template; //TODO store some metrics to allocate buffers later + + RawTextWidget(String template, EvaluatorCompiler compiler) throws ExpressionCompileException { + this.template = template; + } + + public void render(Object bound, Respond respond) { + respond.write(template); + } + + public Set collect(Class clazz) { + return Collections.emptySet(); + } +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/RepeatWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/RepeatWidget.java new file mode 100644 index 00000000..d9e6d74f --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/RepeatWidget.java @@ -0,0 +1,92 @@ +package com.google.sitebricks.rendering.control; + +import com.google.inject.Inject; +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.compiler.Parsing; +import com.google.sitebricks.conversion.TypeConverter; +import com.google.sitebricks.rendering.EmbedAs; +import net.jcip.annotations.Immutable; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Immutable +@EmbedAs("Repeat") +class RepeatWidget implements Renderable { + private final WidgetChain widgetChain; + private final String items; + private final String var; + private final String pageVar; + private final Evaluator evaluator; + private TypeConverter converter; + + private static final String DEFAULT_PAGEVAR = "__page"; + private static final String DEFAULT_VAR = "__this"; + + public RepeatWidget(WidgetChain widgetChain, String expression, Evaluator evaluator) { + this.widgetChain = widgetChain; + + final Map map = Parsing.toBindMap(expression); + this.items = map.get("items"); + String var = map.get("var"); + + if (null != var) + this.var = Parsing.stripQuotes(var); + else + this.var = DEFAULT_VAR; + + //by default the page comes in as __page + String pageVar = map.get("pageVar"); + if (null == pageVar) + pageVar = DEFAULT_PAGEVAR; + else + pageVar = Parsing.stripQuotes(pageVar); + + this.pageVar = pageVar; + this.evaluator = evaluator; + } + + @Inject + public void setConverter(TypeConverter converter) + { + this.converter = converter; + } + + public void render(Object bound, Respond respond) { + + Object value = evaluator.evaluate(items, bound); + + //do nothing if the collection is unavailable for some reason + if (null == value) + return; + + Collection items = converter.convert(value, Collection.class); + + Map context = new HashMap(); + + //set up context variables + int i = 0; + for (Object thing : items) { + + //decorate with some context + context.put(var, thing); + context.put(pageVar, bound); + context.put("index", i++); + context.put("isLast", i == items.size()); + widgetChain.render(context, respond); + } + + } + + + public Set collect(Class clazz) { + return widgetChain.collect(clazz); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/RequireWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/RequireWidget.java new file mode 100644 index 00000000..c6177d4b --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/RequireWidget.java @@ -0,0 +1,42 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.Token; +import com.google.sitebricks.rendering.SelfRendering; + +import net.jcip.annotations.Immutable; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Immutable +@SelfRendering +class RequireWidget implements Renderable { + private final List template; + + public RequireWidget(String xml, EvaluatorCompiler compiler) throws ExpressionCompileException { + this.template = compiler.tokenizeAndCompile(xml); + } + + public void render(Object bound, Respond respond) { + //rebuild template from tokens + StringBuilder builder = new StringBuilder(); + for (Token token : template) { + builder.append(token.render(bound)); + } + + //special method interns tokens + respond.require(builder.toString()); + } + + public Set collect(Class clazz) { + return Collections.emptySet(); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ShowIfWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ShowIfWidget.java new file mode 100644 index 00000000..7273a0b0 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/ShowIfWidget.java @@ -0,0 +1,39 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.rendering.EmbedAs; +import net.jcip.annotations.Immutable; + +import java.util.Set; + + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Immutable @EmbedAs("ShowIf") +class ShowIfWidget implements Renderable { + private final WidgetChain widgetChain; + private final String expression; + private final Evaluator evaluator; + + public ShowIfWidget(WidgetChain widgetChain, String expression, Evaluator evaluator) { + this.widgetChain = widgetChain; + this.expression = expression; + this.evaluator = evaluator; + } + + public void render(Object bound, Respond respond) { + //messy =( + final Object o = evaluator.evaluate(expression, bound); + + if ((Boolean) o) + widgetChain.render(bound, respond); + } + + + public Set collect(Class clazz) { + return widgetChain.collect(clazz); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/SingletonWidgetChain.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/SingletonWidgetChain.java new file mode 100644 index 00000000..7e619462 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/SingletonWidgetChain.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import net.jcip.annotations.Immutable; + +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Immutable +class SingletonWidgetChain implements WidgetChain { + private final Renderable widget; + + public SingletonWidgetChain(Renderable widget) { + this.widget = widget; + } + + public void render(Object bound, Respond respond) { + widget.render(bound, respond); + } + + public WidgetChain addWidget(Renderable renderable) { + throw new IllegalStateException("Cannot add children to singleton widget chain"); + } + + public synchronized Set collect(Class clazz) { + return widget.collect(clazz); + } +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/TerminalWidgetChain.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/TerminalWidgetChain.java new file mode 100644 index 00000000..06c760ed --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/TerminalWidgetChain.java @@ -0,0 +1,30 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import net.jcip.annotations.NotThreadSafe; + +import java.util.Collections; +import java.util.Set; + +/** + *

+ * + * Marker represents the end of a widget chain/branch + *

+ * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@NotThreadSafe +class TerminalWidgetChain implements WidgetChain { + + public void render(Object bound, Respond respond) { } + + public Set collect(Class clazz) { + return Collections.emptySet(); + } + + public WidgetChain addWidget(Renderable renderable) { return this; } + + +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/TextFieldWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/TextFieldWidget.java new file mode 100644 index 00000000..800f0521 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/TextFieldWidget.java @@ -0,0 +1,36 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.rendering.SelfRendering; +import net.jcip.annotations.Immutable; + +import java.util.Collections; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Immutable +@SelfRendering +class TextFieldWidget implements Renderable { + private final WidgetChain widgetChain; + private final String expression; + private final Evaluator evaluator; + + public TextFieldWidget(WidgetChain widgetChain, String expression, Evaluator evaluator) { + this.widgetChain = widgetChain; + this.expression = expression; + this.evaluator = evaluator; + } + + public void render(Object bound, Respond respond) { + respond.withHtml() + .textField(expression, (String) evaluator.evaluate(expression, bound)); + } + + public Set collect(Class clazz) { + return Collections.emptySet(); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/TextWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/TextWidget.java new file mode 100644 index 00000000..550ce8bd --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/TextWidget.java @@ -0,0 +1,43 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.Token; +import com.google.sitebricks.rendering.SelfRendering; +import net.jcip.annotations.ThreadSafe; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ThreadSafe @SelfRendering +class TextWidget implements Renderable { + private final List tokenizedTemplate; //TODO store some metrics to allocate buffers later + + TextWidget(String template, EvaluatorCompiler compiler) throws ExpressionCompileException { + + //compile token stream + tokenizedTemplate = compiler.tokenizeAndCompile(template); + } + + public void render(Object bound, Respond respond) { + + //render template from tokens + StringBuilder builder = new StringBuilder(); + for (Token token : tokenizedTemplate) { + builder.append(token.render(bound)); + } + + respond.write(builder.toString()); + } + + + public Set collect(Class clazz) { + return Collections.emptySet(); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetChain.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetChain.java new file mode 100644 index 00000000..7f3c2994 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetChain.java @@ -0,0 +1,10 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Renderable; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public interface WidgetChain extends Renderable { + WidgetChain addWidget(Renderable renderable); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetRegistry.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetRegistry.java new file mode 100644 index 00000000..f84f44c0 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetRegistry.java @@ -0,0 +1,45 @@ +package com.google.sitebricks.rendering.control; + +import com.google.inject.ImplementedBy; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.RepeatToken; + +import java.util.Map; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@ImplementedBy(DefaultWidgetRegistry.class) +public interface WidgetRegistry { + void add(String key, Class widget); + + boolean isSelfRendering(String widget); + + RepeatToken parseRepeat(String expression); + + XmlWidget xmlWidget(WidgetChain childsChildren, String elementName, Map attribs, + EvaluatorCompiler compiler) throws ExpressionCompileException; + + Renderable newWidget(String key, String expression, WidgetChain widgetChain, + EvaluatorCompiler compiler) throws ExpressionCompileException; + + Renderable requireWidget(String template, EvaluatorCompiler compiler) + throws ExpressionCompileException; + + Renderable textWidget(String template, EvaluatorCompiler compiler) + throws ExpressionCompileException; + + Renderable rawTextWidget(String template, EvaluatorCompiler compiler) + throws ExpressionCompileException; + + Renderable xmlDirectiveWidget(String wholeDeclaration, EvaluatorCompiler evaluatorCompiler) throws ExpressionCompileException; + + void addEmbed(String embedAs); + + void addArgument(String callWith); + + Renderable headWidget(WidgetChain childsChildren, Map attribs, + EvaluatorCompiler evaluatorCompiler) throws ExpressionCompileException; +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetWrapper.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetWrapper.java new file mode 100644 index 00000000..f780923e --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/WidgetWrapper.java @@ -0,0 +1,113 @@ +package com.google.sitebricks.rendering.control; + +import com.google.common.base.Objects; +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.rendering.SelfRendering; +import com.google.sitebricks.routing.PageBook; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +class WidgetWrapper { + private final Class clazz; + private final Constructor constructor; + private final String key; + private final boolean selfRendering; + private final WidgetKind kind; + + private WidgetWrapper(Class clazz, + Constructor constructor, WidgetKind kind, String key) { + this.kind = kind; + this.clazz = clazz; + this.constructor = constructor; + this.key = key; + + selfRendering = clazz.isAnnotationPresent(SelfRendering.class); + } + + public Renderable newWidget(WidgetChain widgetChain, String expression, Evaluator evaluator, + PageBook pageBook) { + try { + return WidgetKind.NORMAL.equals(kind) ? + constructor.newInstance(widgetChain, expression, evaluator) : + constructor.newInstance(toArguments(widgetChain), expression, evaluator, pageBook, key); + + } catch (IllegalAccessException e) { + throw new IllegalStateException("Malformed Widget (this should never happen): " + clazz); + } catch (InvocationTargetException e) { + throw new IllegalStateException("Could not construct an instance of " + clazz, e); + } catch (InstantiationException e) { + throw new IllegalStateException("Could not construct an instance of : " + clazz, e); + } + } + + private static Map toArguments(WidgetChain widgetChain) { + Set arguments = widgetChain.collect(ArgumentWidget.class); + Map map = new HashMap(); + + for (ArgumentWidget argument : arguments) { + map.put(argument.getName(), argument); + } + + return map; + } + + public static WidgetWrapper forWidget(String key, Class widgetClass) { + WidgetKind kind = EmbedWidget.class.isAssignableFrom(widgetClass) + ? WidgetKind.EMBED + : WidgetKind.NORMAL; + Constructor constructor; + + try { + switch (kind) { + case EMBED: + constructor = widgetClass.getConstructor(Map.class, String.class, Evaluator.class, + PageBook.class, String.class); + break; + + case NORMAL: + default: + constructor = widgetClass.getConstructor(WidgetChain.class, String.class, + Evaluator.class); + } + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Malformed Widget (this should never happen): " + + widgetClass); + } + + + // Ugh... + if (!constructor.isAccessible()) + constructor.setAccessible(true); + + return new WidgetWrapper(widgetClass, constructor, kind, key); + } + + /** + * Returns true if this widget will render an outer, containing tag, + * discarding the annotated tag. + */ + public boolean isSelfRendering() { + return selfRendering; + } + + @Override + public String toString() { + return Objects.toStringHelper(WidgetWrapper.class) + .add("key", key) + .add("class", clazz) + .add("kind", kind) + .toString(); + } + + private static enum WidgetKind { + NORMAL, EMBED + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/XmlDirectiveWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/XmlDirectiveWidget.java new file mode 100644 index 00000000..c1c5129a --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/XmlDirectiveWidget.java @@ -0,0 +1,39 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.Parsing; +import com.google.sitebricks.compiler.Token; +import com.google.sitebricks.rendering.SelfRendering; +import net.jcip.annotations.ThreadSafe; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * For stuff like Doctype decls at the top of a file. + */ +@ThreadSafe +@SelfRendering +class XmlDirectiveWidget implements Renderable { + private final List tokens; + + XmlDirectiveWidget(String template, EvaluatorCompiler compiler) throws ExpressionCompileException { + this.tokens = Parsing.tokenize(template, compiler); + } + + public void render(Object bound, Respond respond) { + respond.write(""); + } + + public Set collect(Class clazz) { + return Collections.emptySet(); + } +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/XmlWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/XmlWidget.java new file mode 100644 index 00000000..8d131b5f --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/XmlWidget.java @@ -0,0 +1,137 @@ +package com.google.sitebricks.rendering.control; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.Token; +import com.google.sitebricks.rendering.Attributes; +import com.google.sitebricks.rendering.SelfRendering; +import net.jcip.annotations.ThreadSafe; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +/** + *

Widget renders an XML-like tag

+ * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ThreadSafe +@SelfRendering +class XmlWidget implements Renderable { + private final WidgetChain widgetChain; + private final boolean selfClosed; + private final String name; + private final Map> attributes; + + // HACK Extremely ouch! Replace with Assisted inject. + private static volatile Provider request; + + private static final Set CONTEXTUAL_ATTRIBS; + + static { + Set set = new HashSet(); + + set.add("href"); + set.add("action"); + set.add("src"); + + CONTEXTUAL_ATTRIBS = Collections.unmodifiableSet(set); + } + + + XmlWidget(WidgetChain widgetChain, String name, EvaluatorCompiler compiler, + @Attributes Map attributes) throws ExpressionCompileException { + this.widgetChain = widgetChain; + this.name = name; + this.attributes = Collections.unmodifiableMap(compile(attributes, compiler)); + + //hacky. Script tags should not be self-closed due to IE insanity. + this.selfClosed = + widgetChain instanceof TerminalWidgetChain && !"script".equalsIgnoreCase(name); + } + + //compiles a map of name:value attrs into a map of name:token renderables + static Map> compile(Map attributes, + EvaluatorCompiler compiler) + throws ExpressionCompileException { + + Map> map = new LinkedHashMap>(); + + for (Map.Entry attribute : attributes.entrySet()) { + map.put(attribute.getKey(), compiler.tokenizeAndCompile(attribute.getValue())); + } + + return map; + } + + public void render(Object bound, Respond respond) { + writeOpenTag(bound, respond, name, attributes); + + //write children + if (selfClosed) { + respond.write("/>"); //write self-closed tag + } else { + respond.write('>'); + widgetChain.render(bound, respond); + + //close tag + respond.write("'); + } + } + + static void writeOpenTag(Object bound, Respond respond, String name, + Map> attributes) { + respond.write('<'); + respond.write(name); + + respond.write(' '); + + //write attributes + for (Map.Entry> attribute : attributes.entrySet()) { + respond.write(attribute.getKey()); + respond.write("=\""); + + final List tokenList = attribute.getValue(); + for (int i = 0; i < tokenList.size(); i++) { + Token token = tokenList.get(i); + + if (token.isExpression()) { + respond.write(token.render(bound)); + } else { + respond.write( + contextualizeIfNeeded(attribute.getKey(), (0 == i), (String) token.render(bound))); + } + } + + respond.write("\" "); + } + + respond.chew(); + } + + private static String contextualizeIfNeeded(String attribute, boolean isFirstToken, String raw) { + if (isFirstToken && CONTEXTUAL_ATTRIBS.contains(attribute)) { + //add context to path if needed + if (raw.startsWith("/")) + raw = request.get().getContextPath() + raw; + } + + return raw; + } + + + public Set collect(Class clazz) { + return widgetChain.collect(clazz); + } + + @Inject + public void setRequestProvider(Provider requestProvider) { + XmlWidget.request = requestProvider; + } +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/Assets.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/Assets.java new file mode 100644 index 00000000..d7f4448b --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/Assets.java @@ -0,0 +1,27 @@ +package com.google.sitebricks.rendering.resource; + +import com.google.sitebricks.Export; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

+ * Use to export multiple @Export resources. + *

+ * + *
+ * @Assets({@Export(at="/my.js", "my.js"), ... })
+ * public class MyWebPage { .. }
+ * 
+ * + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Assets { + Export[] value(); +} \ No newline at end of file diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ClasspathResourcesService.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ClasspathResourcesService.java new file mode 100644 index 00000000..18c7a1e1 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ClasspathResourcesService.java @@ -0,0 +1,179 @@ +package com.google.sitebricks.rendering.resource; + +import com.google.common.collect.MapMaker; +import com.google.inject.Singleton; +import com.google.sitebricks.Export; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import net.jcip.annotations.ThreadSafe; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@ThreadSafe +@Singleton +class ClasspathResourcesService implements ResourcesService { + private final Map resources = new MapMaker().makeMap(); + + private static final AtomicReference> mimes = + new AtomicReference>(); + + private static final String DEFAULT_MIME = "__defaultMimeType"; + + public ClasspathResourcesService() { + if (null == mimes.get()) { + final Properties properties = new Properties(); + try { + properties.load( + ClasspathResourcesService.class.getResourceAsStream("mimetypes.properties")); + } catch (IOException e) { + throw new ResourceLoadingException("Can't find mimetypes.properties", e); + } + + //noinspection unchecked + mimes.compareAndSet(null, (Map) properties); + } + } + + public void add(Class clazz, Export export) { + resources.put(export.at(), new Resource(export, clazz)); + } + + public Respond serve(String uri) { + final Resource resource = resources.get(uri); + + //nothing registered + if (null == resource) { + return null; + } + + //load and render resource to responder + return new StaticResourceRespond(resource); + } + + + static String mimeOf(String file) { + final Map mimeTypes = mimes.get(); + for (Map.Entry mime : mimeTypes.entrySet()) { + if (file.matches(mime.getKey())) + return mime.getValue(); + } + + //no match, use the default? + return mimeTypes.get(DEFAULT_MIME); + } + + private static class Resource { + private final Export export; + private final Class clazz; + private final String mimeType; + + private Resource(Export export, Class clazz) { + this.export = export; + this.clazz = clazz; + + this.mimeType = mimeOf(export.resource()); + } + + public String toString() { + return new StringBuilder() + .append("Resource {") + .append("export=") + .append(export) + .append(", class=") + .append(clazz).append('}') + + .toString(); + } + } + + private static class StaticResourceRespond implements Respond { + private final Resource resource; + + public StaticResourceRespond(Resource resource) { + this.resource = resource; + } + + public String getContentType() { + return resource.mimeType; + } + + @Override + public String toString() { + //load and render + List list; + try { + final InputStream stream = resource.clazz.getResourceAsStream(resource.export.resource()); + + if (null == stream) + throw new ResourceLoadingException( + "Couldn't find static resource (did you spell it right?) specified by: " + + resource); + + list = IOUtils.readLines(stream); + } catch (IOException e) { + throw new ResourceLoadingException( + "Error loading static resource specified by: " + resource, e); + } + + StringBuilder buffer = new StringBuilder(); + for (Object o : list) { + buffer.append((String) o); + } + + return buffer.toString(); + } + + public void write(String text) { + throw new UnsupportedOperationException("Static resource responders can't be written to"); + } + + public HtmlTagBuilder withHtml() { + throw new UnsupportedOperationException("Static resource responders can't be written to"); + } + + public void write(char c) { + throw new UnsupportedOperationException("Static resource responders can't be written to"); + } + + public void chew() { + throw new UnsupportedOperationException("Static resource responders can't be written to"); + } + + public void writeToHead(String text) { + throw new UnsupportedOperationException("Static resource responders can't be written to"); + } + + public void require(String requireString) { + throw new UnsupportedOperationException("Static resource responders can't be written to"); + } + + public void redirect(String to) { + throw new UnsupportedOperationException("Static resource responders can't be written to"); + } + + public String getRedirect() { + return null; + } + + public Renderable include(String argument) { + return null; + } + + public String getHead() { + return null; + } + + @Override + public void clear() { + } + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ResourceLoadingException.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ResourceLoadingException.java new file mode 100644 index 00000000..589f6c98 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ResourceLoadingException.java @@ -0,0 +1,14 @@ +package com.google.sitebricks.rendering.resource; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +class ResourceLoadingException extends RuntimeException { + public ResourceLoadingException(String msg, Throwable cause) { + super(msg, cause); + } + + public ResourceLoadingException(String msg) { + super(msg); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ResourcesService.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ResourcesService.java new file mode 100644 index 00000000..a7c216c8 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/resource/ResourcesService.java @@ -0,0 +1,15 @@ +package com.google.sitebricks.rendering.resource; + +import com.google.inject.ImplementedBy; +import com.google.sitebricks.Respond; +import com.google.sitebricks.Export; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@ImplementedBy(ClasspathResourcesService.class) +public interface ResourcesService { + void add(Class clazz, Export export); + + Respond serve(String uri); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/Action.java b/sitebricks/src/main/java/com/google/sitebricks/routing/Action.java new file mode 100644 index 00000000..013b6645 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/Action.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.routing; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * An abstract representation of the service code called + * when a request is processed. Typically maps to a method annotated + * with @Get or something like that. Can be replaced with a SPI to + * create dynamic behavior. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public interface Action { + /** + * Returns true if this action should be called on this request. + * All dispatch rules have succeeded and this is a last-resort + * gate (for example, to handle special headers or gate IPs etc.). + */ + boolean shouldCall(HttpServletRequest request); + + /** + * Invoke this action! + * + * @param page The page object on which to call this action. Aka: + * the 'resource'. + * @param map A map of path variables (fragments) to their values. + * @return an instance of Reply, Redirect or null to trigger a 500 error. + */ + Object call(Object page, Map map); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java b/sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java new file mode 100644 index 00000000..24954c3e --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java @@ -0,0 +1,778 @@ +package com.google.sitebricks.routing; + +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.MapMaker; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.inject.BindingAnnotation; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; +import com.google.inject.name.Named; +import com.google.sitebricks.ActionDescriptor; +import com.google.sitebricks.At; +import com.google.sitebricks.Bricks; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.conversion.TypeConverter; +import com.google.sitebricks.headless.Service; +import com.google.sitebricks.http.Select; +import com.google.sitebricks.http.negotiate.ContentNegotiator; +import com.google.sitebricks.http.negotiate.Negotiation; +import com.google.sitebricks.rendering.Strings; +import com.google.sitebricks.rendering.control.DecorateWidget; +import net.jcip.annotations.GuardedBy; +import net.jcip.annotations.ThreadSafe; +import org.jetbrains.annotations.Nullable; + +import javax.servlet.http.HttpServletRequest; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReference; + +/** + * contains active uri/widget mappings + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ThreadSafe @Singleton +public class DefaultPageBook implements PageBook { + //multimaps TODO refactor to multimap? + + @GuardedBy("lock") // All three following fields + private final Map> pages = Maps.newHashMap(); + private final List universalMatchingPages = Lists.newArrayList(); + private final Map pagesByName = Maps.newHashMap(); + + private final ConcurrentMap, PageTuple> classToPageMap = + new MapMaker() + .weakKeys() + .weakValues() + .makeMap(); + + private final Object lock = new Object(); + private final Injector injector; + + @Inject + public DefaultPageBook(Injector injector) { + this.injector = injector; + } + + @Override @SuppressWarnings("unchecked") + public Collection> getPageMap() { + return (Collection) pages.values(); + } + + public Page serviceAt(String uri, Class pageClass) { + // Handle subpaths, registering each as a separate instance of the page + // tuple. + for (Method method : pageClass.getDeclaredMethods()) { + if (method.isAnnotationPresent(At.class)) { + + // This is a subpath expression. + At at = method.getAnnotation(At.class); + String subpath = at.value(); + + // Validate subpath + if (!subpath.startsWith("/") || subpath.isEmpty() || subpath.length() == 1) { + throw new IllegalArgumentException(String.format( + "Subpath At(\"%s\") on %s.%s() must begin with a \"/\" and must not be empty", + subpath, pageClass.getName(), method.getName())); + } + + subpath = uri + subpath; + + // Register as headless web service. + at(subpath, pageClass, true); + } + } + + return at(uri, pageClass, true); + } + + public PageTuple at(String uri, Class clazz) { + return at(uri, clazz, clazz.isAnnotationPresent(Service.class)); + } + + @Override + public void at(String uri, List actionDescriptors, + Map, String> methodSet) { + Multimap actions = HashMultimap.create(); + + for (ActionDescriptor actionDescriptor : actionDescriptors) { + for (Class method : actionDescriptor.getMethods()) { + String methodString = methodSet.get(method); + Action action = actionDescriptor.getAction(); + + if (null == action) { + action = injector.getInstance(actionDescriptor.getActionKey()); + } else { + injector.injectMembers(action); + } + + actions.put(methodString, new SpiAction(action, actionDescriptor)); + } + } + + // Register into the book! + at(new PageTuple(uri, new PathMatcherChain(uri), null, true, false, injector, actions)); + } + + private void at(PageTuple page) { + // Is Universal? + synchronized (lock) { + String key = firstPathElement(page.getUri()); + if (isVariable(key)) { + universalMatchingPages.add(page); + } else { + multiput(pages, key, page); + } + } + + // Actions are not backed by classes. + if (page.pageClass() != null) + classToPageMap.put(page.pageClass(), page); + } + + private PageTuple at(String uri, Class clazz, boolean headless) { + final String key = firstPathElement(uri); + final PageTuple pageTuple = + new PageTuple(uri, new PathMatcherChain(uri), clazz, injector, headless, false); + + synchronized (lock) { + //is universal? (i.e. first element is a variable) + if (isVariable(key)) + universalMatchingPages.add(pageTuple); + else { + multiput(pages, key, pageTuple); + } + } + + // Does not need to be inside lock, as it is concurrent. + classToPageMap.put(clazz, pageTuple); + + return pageTuple; + } + + public Page embedAs(Class clazz, String as) { + Preconditions.checkArgument(null == clazz.getAnnotation(Service.class), + "You cannot embed headless web services!"); + PageTuple pageTuple = new PageTuple("", PathMatcherChain.ignoring(), clazz, injector, false, false); + + synchronized (lock) { + pagesByName.put(as.toLowerCase(), pageTuple); + } + + return pageTuple; + } + + public Page decorate(Class pageClass) { + Preconditions.checkArgument(null == pageClass.getAnnotation(Service.class), + "You cannot extend headless web services!"); + PageTuple pageTuple = new PageTuple("", PathMatcherChain.ignoring(), pageClass, injector, false, true); + + // store page with a special name used by ExtendWidget + String name = DecorateWidget.embedNameFor(pageClass); + synchronized (lock) { + pagesByName.put(name, pageTuple); + } + + return pageTuple; + } + + public Page nonCompilingGet(String uri) { + // The regular get is non compiling, in our case. So these methods are identical. + return get(uri); + } + + private static void multiput(Map> pages, String key, + PageTuple page) { + List list = pages.get(key); + + if (null == list) { + list = new ArrayList(); + pages.put(key, list); + } + + list.add(page); + } + + private static boolean isVariable(String key) { + return key.length() > 0 && ':' == key.charAt(0); + } + + String firstPathElement(String uri) { + String shortUri = uri.substring(1); + + final int index = shortUri.indexOf("/"); + + return (index >= 0) ? shortUri.substring(0, index) : shortUri; + } + + @Nullable + public Page get(String uri) { + final String key = firstPathElement(uri); + + List tuple = pages.get(key); + + //first try static first piece + if (null != tuple) { + + //first try static first piece + for (PageTuple pageTuple : tuple) { + if (pageTuple.matcher.matches(uri)) + return pageTuple; + } + } + + //now try dynamic first piece (how can we make this faster?) + for (PageTuple pageTuple : universalMatchingPages) { + if (pageTuple.matcher.matches(uri)) + return pageTuple; + } + + //nothing matched + return null; + } + + public Page forName(String name) { + return pagesByName.get(name); + } + + @Nullable + public Page forInstance(Object instance) { + Class aClass = instance.getClass(); + PageTuple targetType = classToPageMap.get(aClass); + + // Do a super crawl to detect the target type. + while (null == targetType) { + aClass = aClass.getSuperclass(); + targetType = classToPageMap.get(aClass); + + // Stop at the root =D + if (Object.class.equals(aClass)) { + return null; + } + } + + return InstanceBoundPage.delegating(targetType, instance); + } + + public Page forClass(Class pageClass) { + return classToPageMap.get(pageClass); + } + + public static class InstanceBoundPage implements Page { + private final Page delegate; + private final Object instance; + + private InstanceBoundPage(Page delegate, Object instance) { + this.delegate = delegate; + this.instance = instance; + } + + public Renderable widget() { + return delegate.widget(); + } + + public Object instantiate() { + return instance; + } + + public Object doMethod(String httpMethod, Object page, String pathInfo, + HttpServletRequest request) { + return delegate.doMethod(httpMethod, page, pathInfo, request); + } + + public Class pageClass() { + return delegate.pageClass(); + } + + public void apply(Renderable widget) { + delegate.apply(widget); + } + + public String getUri() { + return delegate.getUri(); + } + + public boolean isHeadless() { + return delegate.isHeadless(); + } + + @Override + public boolean isDecorated() { + return delegate.isDecorated(); + } + + public Set getMethod() { + return delegate.getMethod(); + } + + public int compareTo(Page page) { + return delegate.compareTo(page); + } + + public static InstanceBoundPage delegating(Page delegate, Object instance) { + return new InstanceBoundPage(delegate, instance); + } + } + + @Select("") //the default select (hacky!!) + public static class PageTuple implements Page { + private final String uri; + private final PathMatcher matcher; + private final AtomicReference pageWidget = new AtomicReference(); + private final Class clazz; + private final boolean headless; + private final boolean extension; + private final Injector injector; + + private final Multimap methods; + + //dispatcher switch (select on request param by default) + private final Select select; + private static final Key>> HTTP_METHODS_KEY = + Key.get(new TypeLiteral>>() {}, Bricks.class); + + // A map of http methods -> annotation types (e.g. "POST" -> @Post) + private Map> httpMethods; + + public PageTuple(String uri, PathMatcher matcher, Class clazz, boolean headless, boolean extension, + Injector injector, Multimap methods) { + this.uri = uri; + this.matcher = matcher; + this.clazz = clazz; + this.headless = headless; + this.extension = extension; + this.injector = injector; + this.methods = methods; + this.select = PageTuple.class.getAnnotation(Select.class); + this.httpMethods = injector.getInstance(HTTP_METHODS_KEY); + } + + public PageTuple(String uri, PathMatcher matcher, Class clazz, Injector injector, + boolean headless, boolean extension) { + this.uri = uri; + this.matcher = matcher; + this.clazz = clazz; + this.injector = injector; + this.headless = headless; + this.extension = extension; + + this.select = discoverSelect(clazz); + + this.httpMethods = injector.getInstance(HTTP_METHODS_KEY); + this.methods = reflectAndCache(uri, httpMethods); + } + + //the @Select request parameter-based event dispatcher + private Select discoverSelect(Class clazz) { + final Select select = clazz.getAnnotation(Select.class); + if (null != select) + return select; + else + return PageTuple.class.getAnnotation(Select.class); + } + + /** + * Returns a map of HTTP-method name to @Annotation-marked methods + */ + @SuppressWarnings({"JavaDoc"}) + private Multimap reflectAndCache(String uri, + Map> methodMap) { + String tail = ""; + if (clazz.isAnnotationPresent(At.class)) { + int length = clazz.getAnnotation(At.class).value().length(); + + // It's possible that the uri being registered is shorter than the + // class length, this can happen in the case of using the .at() module + // directive to override @At() URI path mapping. In this case we treat + // this call as a top-level path registration with no tail. Any + // encountered subpath @At methods will be ignored for this URI. + if (uri != null && length <= uri.length()) + tail = uri.substring(length); + } + + Multimap map = HashMultimap.create(); + + for (Map.Entry> entry : methodMap.entrySet()) { + + Class get = entry.getValue(); + // First search any available public methods and store them (including inherited ones) + for (Method method : clazz.getMethods()) { + if (method.isAnnotationPresent(get)) { + if (!method.isAccessible()) + method.setAccessible(true); //ugh + + // Be defensive about subpaths. + if (method.isAnnotationPresent(At.class)) { + // Skip any at-annotated methods for a top-level path registration. + if (tail.isEmpty()) { + continue; + } + + // Skip any at-annotated methods that do not exactly match the path. + if (!tail.equals(method.getAnnotation(At.class).value())) { + continue; + } + } else if (!tail.isEmpty()) { + // If this is the top-level method we're scanning, but their is a tail, i.e. + // this is not intended to be served by the top-level method, then skip. + continue; + } + + // Otherwise register this method for firing... + + //remember default value is empty string + String value = getValue(get, method); + String key = (Strings.empty(value)) ? entry.getKey() : entry.getKey() + value; + map.put(key, new MethodTuple(method, injector)); + } + } + + // Then search class's declared methods only (these take precedence) + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(get)) { + if (!method.isAccessible()) + method.setAccessible(true); //ugh + + // Be defensive about subpaths. + if (method.isAnnotationPresent(At.class)) { + // Skip any at-annotated methods for a top-level path registration. + if (tail.isEmpty()) { + continue; + } + + // Skip any at-annotated methods that do not exactly match the path. + if (!tail.equals(method.getAnnotation(At.class).value())) { + continue; + } + } else if (!tail.isEmpty()) { + // If this is the top-level method we're scanning, but their is a tail, i.e. + // this is not intended to be served by the top-level method, then skip. + continue; + } + + // Otherwise register this method for firing... + + //remember default value is empty string + String value = getValue(get, method); + String key = (Strings.empty(value)) ? entry.getKey() : entry.getKey() + value; + map.put(key, new MethodTuple(method, injector)); + } + } + } + + return map; + } + + private String getValue(Class get, Method method) { + return readAnnotationValue(method.getAnnotation(get)); + } + + public Renderable widget() { + return pageWidget.get(); + } + + public Object instantiate() { + return clazz == null ? Collections.emptyMap() : injector.getInstance(clazz); + } + + public boolean isHeadless() { + return headless; + } + + @Override + public boolean isDecorated() { + return extension; + } + + public Set getMethod() { + return methods.keySet(); + } + + public int compareTo(Page page) { + return uri.compareTo(page.getUri()); + } + + public Object doMethod(String httpMethod, Object page, String pathInfo, + HttpServletRequest request) { + + //nothing to fire + if (Strings.empty(httpMethod)) { + return null; + } + + @SuppressWarnings("unchecked") // Guaranteed by javax.servlet + Map params = (Map) request.getParameterMap(); + + // Extract injectable pieces of the pathInfo. + final Map map = matcher.findMatches(pathInfo); + + //find method(s) to dispatch + // to + final String[] events = params.get(select.value()); + if (null != events) { + boolean matched = false; + for (String event : events) { + String key = httpMethod + event; + Collection tuples = methods.get(key); + Object redirect = null; + + if (null != tuples) { + for (Action action : tuples) { + if (action.shouldCall(request)) { + matched = true; + redirect = action.call(page, map); + break; + } + } + } + + //redirects interrupt the event dispatch sequence. Note this might cause inconsistent behaviour depending on + // the order of processing for events. + if (null != redirect) { + return redirect; + } + } + + // no matched events. Fire default handler + if (!matched) { + return callAction(httpMethod, page, map, request); + } + + } else { + // Fire default handler (no events defined) + return callAction(httpMethod, page, map, request); + } + + //no redirects, render normally + return null; + } + + private Object callAction(String httpMethod, Object page, Map pathMap, + HttpServletRequest request) { + + // There may be more than one default handler + Collection tuple = methods.get(httpMethod); + Object redirect = null; + if (null != tuple) { + for (Action action : tuple) { + if (action.shouldCall(request)) { + redirect = action.call(page, pathMap); + break; + } + } + } + return redirect; + + } + + public Class pageClass() { + return clazz; + } + + public void apply(Renderable widget) { + this.pageWidget.set(widget); + } + + public String getUri() { + return uri; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Page)) return false; + + Page that = (Page) o; + + return this.clazz.equals(that.pageClass()); + } + + @Override + public int hashCode() { + return clazz.hashCode(); + } + } + + private static class MethodTuple implements Action { + private final Method method; + private final Injector injector; + private final List args; + private final Map negotiates; + private final ContentNegotiator negotiator; + private final TypeConverter converter; + + private MethodTuple(Method method, Injector injector) { + this.method = method; + this.injector = injector; + this.args = reflect(method); + this.negotiates = discoverNegotiates(method, injector); + this.negotiator = injector.getInstance(ContentNegotiator.class); + this.converter = injector.getInstance(TypeConverter.class); + } + + private List reflect(Method method) { + final Annotation[][] annotationsGrid = method.getParameterAnnotations(); + if (null == annotationsGrid) + return Collections.emptyList(); + + List args = new ArrayList(); + for (int i = 0; i < annotationsGrid.length; i++) { + Annotation[] annotations = annotationsGrid[i]; + + Annotation bindingAnnotation = null; + boolean namedFound = false; + for (Annotation annotation : annotations) { + if (Named.class.isInstance(annotation)) { + Named named = (Named) annotation; + + args.add(new NamedParameter(named.value(), method.getGenericParameterTypes()[i])); + namedFound = true; + + break; + } else if (annotation.annotationType().isAnnotationPresent(BindingAnnotation.class)) { + bindingAnnotation = annotation; + } + } + + if (!namedFound) { + // Could be an arbitrary injection request. + Class argType = method.getParameterTypes()[i]; + Key key = (null != bindingAnnotation) + ? Key.get(argType, bindingAnnotation) + : Key.get(argType); + + args.add(key); + + if (null == injector.getBindings().get(key)) + throw new InvalidEventHandlerException( + "Encountered an argument not annotated with @Named and not a valid injection key" + + " in event handler method: " + method + " " + key); + } + + } + + return Collections.unmodifiableList(args); + } + + /** + * @return true if this method tuple can be validly called against this request. + * Used to select for content negotiation. + */ + @Override + public boolean shouldCall(HttpServletRequest request) { + return negotiator.shouldCall(negotiates, request); + } + + + @Override + public Object call(Object page, Map map) { + List arguments = new ArrayList(); + for (Object arg : args) { + if (arg instanceof NamedParameter) { + NamedParameter np = (NamedParameter) arg; + String text = map.get(np.getName()); + Object value = converter.convert(text, np.getType()); + arguments.add(value); + } else + arguments.add(injector.getInstance((Key) arg)); + } + + return call(page, method, arguments.toArray()); + } + + private static Object call(Object page, final Method method, + Object[] args) { + try { + return method.invoke(page, args); + } catch (IllegalAccessException e) { + throw new EventDispatchException( + "Could not access event method (appears to be a security problem): " + method, e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + StackTraceElement[] stackTrace = cause.getStackTrace(); + throw new EventDispatchException(String.format( + "Exception [%s - \"%s\"] thrown by event method [%s]\n\nat %s\n" + + "(See below for entire trace.)\n", + cause.getClass().getSimpleName(), + cause.getMessage(), method, + stackTrace[0]), e); + } + } + + //the @Accept request header-based event dispatcher + private Map discoverNegotiates(Method method, Injector injector) { + // This ugly gunk gets us the map of headers to negotiation annotations + Map> negotiationsMap = injector.getInstance( + Key.get(new TypeLiteral>>(){ }, Negotiation.class)); + + Map negotiations = Maps.newHashMap(); + // Gather all the negotiation annotations in this class. + for (Map.Entry> headerAnn : negotiationsMap.entrySet()) { + Annotation annotation = method.getAnnotation(headerAnn.getValue()); + if (annotation != null) { + negotiations.put(headerAnn.getKey(), readAnnotationValue(annotation)); + } + } + + return negotiations; + } + + public class NamedParameter { + private final String name; + private final Type type; + + public NamedParameter(String name, Type type) { + this.name = name; + this.type = type; + } + + public String getName() { + return name; + } + + public Type getType() { + return type; + } + } + + } + + /** + * A simple utility method that reads the String value attribute of any annotation + * instance. + */ + static String readAnnotationValue(Annotation annotation) { + try { + Method m = annotation.getClass().getMethod("value"); + + return (String) m.invoke(annotation); + + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Encountered a configured annotation that " + + "has no value parameter. This should never happen. " + annotation, e); + } catch (InvocationTargetException e) { + throw new IllegalStateException("Encountered a configured annotation that " + + "could not be read." + annotation, e); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Encountered a configured annotation that " + + "could not be read." + annotation, e); + } + } + +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/EventDispatchException.java b/sitebricks/src/main/java/com/google/sitebricks/routing/EventDispatchException.java new file mode 100644 index 00000000..cce80d17 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/EventDispatchException.java @@ -0,0 +1,10 @@ +package com.google.sitebricks.routing; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +class EventDispatchException extends RuntimeException { + public EventDispatchException(String msg, Exception e) { + super(msg, e); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/InMemorySystemMetrics.java b/sitebricks/src/main/java/com/google/sitebricks/routing/InMemorySystemMetrics.java new file mode 100644 index 00000000..a3500398 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/InMemorySystemMetrics.java @@ -0,0 +1,103 @@ +package com.google.sitebricks.routing; + +import com.google.common.collect.MapMaker; +import com.google.inject.Singleton; +import com.google.sitebricks.compiler.CompileError; +import net.jcip.annotations.ThreadSafe; + +import java.util.List; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; + +/** + * This class is completely lock and wait free. It provides the + * "last seen" metrics, optimistically. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ThreadSafe +@Singleton +class InMemorySystemMetrics implements SystemMetrics { + private final ConcurrentMap, Metric> pages = new MapMaker().weakKeys().makeMap(); + private final AtomicBoolean active = new AtomicBoolean(false); + + private final Logger log = Logger.getLogger(SystemMetrics.class.getName()); + + public void logPageRenderTime(Class page, long time) { + Metric metric = putIfAbsent(page); + + // Requests fight it out for who wins this set(). + metric.lastRenderTime.set(time); + } + + public void logErrorsAndWarnings(Class page, List errors, List warnings) { + Metric metric = putIfAbsent(page); + + // These must always be set in unison, so we are forced to use a wrapper to avoid synchronization. + ErrorTuple errorTuple = new ErrorTuple(errors, warnings); + metric.lastErrors.set(errorTuple); + + // Spit out to log. + log.warning(errorTuple.toString()); + } + + public void activate() { + active.set(true); + } + + public boolean isActive() { + return active.get(); + } + + private Metric putIfAbsent(Class page) { + Metric metric = pages.get(page); + + // Concurrent put-if-absent. + if (null == metric) { + Metric newMetric = new Metric(); + + // Attempt to put it using CAS. + final Metric returned = pages.putIfAbsent(page, newMetric); + + // If the put succeeded, use it (otherwise get it from the map). + if (null == returned) + metric = newMetric; + else + metric = returned; + } + + return metric; + } + + /** + * Associates various metrics with a given page It is not guaranteed to represent any + * particular request, rather metrics are collected over time. + *

+ * Except for the errors and warnings, which are always the last ones available (roughly!) + */ + private static class Metric { + private final AtomicLong lastRenderTime = new AtomicLong(0); + private final AtomicReference lastErrors = new AtomicReference(); + + } + + //wrapper helps avoid locking when setting errors and warnings for a page atomically + private static class ErrorTuple { + private final List errors; + private final List warnings; + + public ErrorTuple(List errors, List warnings) { + this.errors = errors; + this.warnings = warnings; + } + + @Override + public String toString() { + return "Template compile summary: errors=" + errors + + ", warnings=" + warnings; + } + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/InvalidEventHandlerException.java b/sitebricks/src/main/java/com/google/sitebricks/routing/InvalidEventHandlerException.java new file mode 100644 index 00000000..7bcbabe0 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/InvalidEventHandlerException.java @@ -0,0 +1,10 @@ +package com.google.sitebricks.routing; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +class InvalidEventHandlerException extends RuntimeException { + public InvalidEventHandlerException(String s) { + super(s); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/PageBook.java b/sitebricks/src/main/java/com/google/sitebricks/routing/PageBook.java new file mode 100644 index 00000000..35e32a97 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/PageBook.java @@ -0,0 +1,142 @@ +package com.google.sitebricks.routing; + +import com.google.inject.AbstractModule; +import com.google.inject.ImplementedBy; +import com.google.inject.Module; +import com.google.inject.Stage; +import com.google.sitebricks.ActionDescriptor; +import com.google.sitebricks.Renderable; + +import javax.servlet.http.HttpServletRequest; +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ImplementedBy(DefaultPageBook.class) +public interface PageBook { + + /** + * Register a page class at the given contextual URI. + * + * @return A {@link Page} representing the given class + * without a compiled template applied. + */ + Page at(String uri, Class myPageClass); + + /** + * + * @param uri A contextual URI where a page (maybe) registered. + * @return A {@link Page} object thatis capable of rend + */ + Page get(String uri); + + Page forName(String name); + + /** + * Registers a page class as an embeddable. + * @param as The annotation name to register this widget as. + * Example: {@code "Hello"} will make this page class + * available for embedding as

{@literal @}Hello
. + */ + Page embedAs(Class pageClass, String as); + + /** + * Indicates that the template for this class is to be + * inserted into a superclass template using @Decorated + * + * @param pageClass + */ + Page decorate(Class pageClass); + + /** + * Same as {@linkplain #get} except guaranteed not to trigger a + * cascading compile of page bricks. + */ + Page nonCompilingGet(String uri); + + /** + * Similar to {@linkplain #get} except that instead of returning + * a page for a URI, it returns the page matching the class of the + * provided instance, and uses the instance itself to deliver the + * page. + * + * @param instance An instance of some page registered by an {@literal @}{@code At} + * annotation or similar method in this sitebricks app. + */ + Page forInstance(Object instance); + + /** + * Very similar to {@linkplain #forInstance(Object)}, except that it + * takes a class literal instead and does NOT do super crawling. + */ + Page forClass(Class pageClass); + + /** + * Same as {@linkplain #at} but registers a headless web service instead. + */ + Page serviceAt(String uri, Class pageClass); + + Collection> getPageMap(); + + void at(String uri, List actionDescriptor, + Map, String> methodSet); + + + public static interface Page extends Comparable { + Renderable widget(); + + Object instantiate(); + + Object doMethod(String httpMethod, Object page, String pathInfo, HttpServletRequest request); + + Class pageClass(); + + void apply(Renderable widget); + + String getUri(); + + boolean isHeadless(); + + boolean isDecorated(); + + Set getMethod(); + } + + public static final class Routing extends AbstractModule { + private Routing() { + } + + @Override + protected final void configure() { + if (Stage.DEVELOPMENT.equals(binder().currentStage())) { + bind(PageBook.class) + .annotatedWith(Production.class) + .to(DefaultPageBook.class); + + bind(RoutingDispatcher.class) + .annotatedWith(Production.class) + .to(WidgetRoutingDispatcher.class); + } + } + + public static Module module() { + return new Routing(); + } + + //Ensures only one instance of the Routine module is installed. + @Override + public boolean equals(Object obj) { + return obj instanceof Routing; + } + + @Override + public int hashCode() { + return Routing.class.hashCode(); + } + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/PathMatcher.java b/sitebricks/src/main/java/com/google/sitebricks/routing/PathMatcher.java new file mode 100644 index 00000000..80e5379b --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/PathMatcher.java @@ -0,0 +1,14 @@ +package com.google.sitebricks.routing; + +import java.util.Map; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +interface PathMatcher { + boolean matches(String incoming); + + String name(); + + Map findMatches(String incoming); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/PathMatcherChain.java b/sitebricks/src/main/java/com/google/sitebricks/routing/PathMatcherChain.java new file mode 100644 index 00000000..ddf854a9 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/PathMatcherChain.java @@ -0,0 +1,141 @@ +package com.google.sitebricks.routing; + +import net.jcip.annotations.Immutable; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Immutable +class PathMatcherChain implements PathMatcher { + private final List path; + private static final String PATH_SEPARATOR = "/"; + + public PathMatcherChain(String path) { + this.path = toMatchChain(path); + } + + //converts a string path to a tree of heterogenous matchers + private static List toMatchChain(String path) { + String[] pieces = path.split(PATH_SEPARATOR); + + List matchers = new ArrayList(); + for (String piece : pieces) { + matchers.add((piece.startsWith(":")) ? new GreedyPathMatcher(piece) : new SimplePathMatcher(piece)); + } + + return Collections.unmodifiableList(matchers); + } + + public String name() { + return null; + } + + public boolean matches(String incoming) { + final Map map = findMatches(incoming); + return null != map; + } + + //TODO this whole path matching algorithm is in linear time, could easily be constant time + public Map findMatches(String incoming) { + String[] pieces = incoming.split(PATH_SEPARATOR); + int i = 0; + + //too many matchers, short circuit + if (path.size() > pieces.length) + return null; + + Map values = new HashMap(); + for (PathMatcher pathMatcher : path) { + + //sanity to prevent fencepost + if (i == pieces.length) + return pathMatcher.matches("") ? values : null; //go greedy on index paths + + String piece = pieces[i]; + + if (!pathMatcher.matches(piece)) + return null; + + //store variable as needed + final String name = pathMatcher.name(); + if (null != name) + values.put(name, piece); + + //next piece + i++; + } + + return (i == pieces.length) ? values : null; + } + + @Immutable + static class SimplePathMatcher implements PathMatcher { + private final String path; + + SimplePathMatcher(String path) { + this.path = path; + } + + public boolean matches(String incoming) { + return path.equals(incoming); + } + + //TODO this whole path matching algorithm is in linear time, could easily be constant time + @NotNull + public Map findMatches(String incoming) { + return Collections.emptyMap(); + } + + public String name() { + return null; + } + } + + //matches anything, i.e. a variable :blah inside a path template + @Immutable + static class GreedyPathMatcher implements PathMatcher { + private final String variable; + + public GreedyPathMatcher(String piece) { + this.variable = piece.substring(1); + } + + public boolean matches(String incoming) { + return true; + } + + @NotNull + public Map findMatches(String incoming) { + return Collections.emptyMap(); + } + + public String name() { + return variable; + } + } + + //matches nothing, i.e. always returns false (used for blocking sitebricks) + @Immutable + static class IgnoringPathMatcher implements PathMatcher { + public boolean matches(String incoming) { + return false; + } + + @NotNull + public Map findMatches(String incoming) { + return Collections.emptyMap(); + } + + public String name() { + return ""; + } + } + + static PathMatcher ignoring() { + return new IgnoringPathMatcher(); + } + +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/Production.java b/sitebricks/src/main/java/com/google/sitebricks/routing/Production.java new file mode 100644 index 00000000..b0c85544 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/Production.java @@ -0,0 +1,13 @@ +package com.google.sitebricks.routing; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) +*/ +@Retention(RetentionPolicy.RUNTIME) +@BindingAnnotation +public @interface Production { } diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/RoutingDispatcher.java b/sitebricks/src/main/java/com/google/sitebricks/routing/RoutingDispatcher.java new file mode 100644 index 00000000..8b72527f --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/RoutingDispatcher.java @@ -0,0 +1,16 @@ +package com.google.sitebricks.routing; + +import com.google.inject.ImplementedBy; +import com.google.sitebricks.Respond; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@ImplementedBy(WidgetRoutingDispatcher.class) +public interface RoutingDispatcher { + Respond dispatch(HttpServletRequest request, HttpServletResponse response) throws IOException; +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/ServiceAction.java b/sitebricks/src/main/java/com/google/sitebricks/routing/ServiceAction.java new file mode 100644 index 00000000..62cb5f97 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/ServiceAction.java @@ -0,0 +1,32 @@ +package com.google.sitebricks.routing; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.headless.Request; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * A simple action that takes a request and returns a Reply which + * a subclass needs to provide. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public abstract class ServiceAction implements Action { + @Inject + private Provider requestProvider; + + @Override + public boolean shouldCall(HttpServletRequest request) { + return true; + } + + @Override + public final Object call(Object page, Map map) { + return call(requestProvider.get(), map); + } + + protected abstract Reply call(Request request, Map pathFragments); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/SpiAction.java b/sitebricks/src/main/java/com/google/sitebricks/routing/SpiAction.java new file mode 100644 index 00000000..21cefc1c --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/SpiAction.java @@ -0,0 +1,56 @@ +package com.google.sitebricks.routing; + +import com.google.sitebricks.ActionDescriptor; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * An Action that is configured using the at().perform() scheme. See + * {@link com.google.sitebricks.SitebricksModule} for details. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class SpiAction implements Action { + private final Action action; + private final Map selectParams; + private final Map selectHeaders; + + public SpiAction(Action action, ActionDescriptor actionDescriptor) { + this.action = action; + selectParams = actionDescriptor.getSelectParams(); + selectHeaders = actionDescriptor.getSelectHeaders(); + } + + @Override + public boolean shouldCall(HttpServletRequest request) { + boolean should; + + if (null != selectParams) { + for (Map.Entry select : selectParams.entrySet()) { + if (!select.getValue().equals(request.getParameter(select.getKey()))) { + should = false; + } + } + } + + if (null != selectHeaders) { + for (Map.Entry header : selectHeaders.entrySet()) { + if (!header.getValue().equals(request.getHeader(header.getKey()))) { + should = false; + } + } + } + + // (JFA) Might be a good idea to pass the value of should as a request attribute + // so an action can see if what was the value before getting invoked and take a decision based on it. + should = action.shouldCall(request); + + return should; + } + + @Override + public Object call(Object page, Map map) { + return action.call(page, map); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/SystemMetrics.java b/sitebricks/src/main/java/com/google/sitebricks/routing/SystemMetrics.java new file mode 100644 index 00000000..55ef3944 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/SystemMetrics.java @@ -0,0 +1,39 @@ +package com.google.sitebricks.routing; + +import com.google.inject.ImplementedBy; +import com.google.sitebricks.compiler.CompileError; + +import java.util.List; + +/** + * Keeps track of various global performance and error metrics. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@ImplementedBy(InMemorySystemMetrics.class) +public interface SystemMetrics { + /** + * Records the last page render time for the given page (in millis). + * This method is concurrent and does not guarantee that the last + * render time accurately reflects the last page delivered to a user. + */ + void logPageRenderTime(Class page, long time); + + /** + * This sets the current errors and warnings list as given, globally. + * This method is thread-safe. + */ + void logErrorsAndWarnings(Class page, List errors, List warnings); + + /** + * Puts the system into a ready state. This is used by Sitebricks to + * determine whether we're in the compile phase. + */ + void activate(); + + /** + * @return Returns true if the application is ready to begin processing + * requests, false if Sitebricks is still in the compile phase. + */ + boolean isActive(); +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java new file mode 100644 index 00000000..aadc1f39 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java @@ -0,0 +1,143 @@ +package com.google.sitebricks.routing; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.sitebricks.Respond; +import com.google.sitebricks.binding.FlashCache; +import com.google.sitebricks.binding.RequestBinder; +import com.google.sitebricks.headless.HeadlessRenderer; +import com.google.sitebricks.rendering.resource.ResourcesService; +import net.jcip.annotations.Immutable; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@Immutable +@Singleton +class WidgetRoutingDispatcher implements RoutingDispatcher { + private final PageBook book; + private final RequestBinder binder; + private final Provider respondProvider; + private final ResourcesService resourcesService; + private final Provider flashCacheProvider; + private final HeadlessRenderer headlessRenderer; + + @Inject + public WidgetRoutingDispatcher(PageBook book, RequestBinder binder, Provider respondProvider, + ResourcesService resourcesService, Provider flashCacheProvider, + HeadlessRenderer headlessRenderer) { + this.headlessRenderer = headlessRenderer; + this.book = book; + this.binder = binder; + this.respondProvider = respondProvider; + this.resourcesService = resourcesService; + this.flashCacheProvider = flashCacheProvider; + } + + public Respond dispatch(HttpServletRequest request, HttpServletResponse response) throws IOException { + String uri = getPathInfo(request); + + //first try dispatching as a static resource service + Respond respond = resourcesService.serve(uri); + + if (null != respond) + return respond; + + // Otherwise try to dispatch as a widget/page + // Check if there is a page chain link sitting here + // for this page. + // NOTE(dhanji): we must use remove, to atomically + // remove the page and process it in one go. It is + // also worth coordinating this with conversation request + // queueing. + // TODO(dhanji): Change flashcache to use temporary cookies instead. + PageBook.Page page = flashCacheProvider.get().remove(uri); + + // If there is no link, obtain page via Guice as normal. + if (null == page) + page = book.get(uri); + + //could not dispatch as there was no match + if (null == page) + return null; + + final Object instance = page.instantiate(); + if (page.isHeadless()) { + respond = Respond.HEADLESS; + + bindAndReply(request, response, page, instance); + } else { + respond = respondProvider.get(); + + //fire events and render reponders + bindAndRespond(request, page, respond, instance); + } + + return respond; + } + + private void bindAndReply(HttpServletRequest request, HttpServletResponse response, + PageBook.Page page, Object instance) throws IOException { + // bind request (sets request params, etc). + binder.bind(request, instance); + + // call the appropriate handler. + headlessRenderer.render(response, fireEvent(request, page, instance)); + + } + + private String getPathInfo(HttpServletRequest request) { + return request.getRequestURI().substring(request.getContextPath().length()); + } + + private void bindAndRespond(HttpServletRequest request, PageBook.Page page, Respond respond, + Object instance) { + //bind request + binder.bind(request, instance); + + //fire get/post events + final Object redirect = fireEvent(request, page, instance); + + //render to respond + if (null != redirect) { + + if (redirect instanceof String) + respond.redirect((String) redirect); + else if (redirect instanceof Class) { + PageBook.Page targetPage = book.forClass((Class) redirect); + + // should never be null coz it is validated on compile. + respond.redirect(contextualize(request, targetPage.getUri())); + } else { + // Handle page-chaining driven redirection. + PageBook.Page targetPage = book.forInstance(redirect); + + // should never be null coz it will be validated at compile time. + flashCacheProvider.get().put(targetPage.getUri(), targetPage); + + // Send to the canonical address of the page. This is also + // verified at compile, not be a variablized matcher. + respond.redirect(contextualize(request, targetPage.getUri())); + } + } else + page.widget().render(instance, respond); + } + + // We're sure the request parameter map is a Map + @SuppressWarnings("unchecked") + private Object fireEvent(HttpServletRequest request, PageBook.Page page, Object instance) { + final String method = request.getMethod(); + final String pathInfo = getPathInfo(request); + + return page.doMethod(method.toLowerCase(), instance, pathInfo, request); + } + + private static String contextualize(HttpServletRequest request, String targetUri) { + return request.getContextPath() + targetUri; + } +} diff --git a/sitebricks/src/main/resources/com/google/sitebricks/core/CaseWidget.html b/sitebricks/src/main/resources/com/google/sitebricks/core/CaseWidget.html new file mode 100644 index 00000000..73a1e447 --- /dev/null +++ b/sitebricks/src/main/resources/com/google/sitebricks/core/CaseWidget.html @@ -0,0 +1,6 @@ + + + @Include(choice) + + + \ No newline at end of file diff --git a/sitebricks/src/main/resources/com/google/sitebricks/core/ll.gif b/sitebricks/src/main/resources/com/google/sitebricks/core/ll.gif new file mode 100644 index 0000000000000000000000000000000000000000..35f64f43d713942be6982dfcc2bda2e18f35c876 GIT binary patch literal 46 ucmZ?wbhEHbWMN=oXkcLY4+e@qSr{1@7#VaJfB+=Jz{Kj3H{(D6gEas?cMKZ< literal 0 HcmV?d00001 diff --git a/sitebricks/src/main/resources/com/google/sitebricks/core/lr.gif b/sitebricks/src/main/resources/com/google/sitebricks/core/lr.gif new file mode 100644 index 0000000000000000000000000000000000000000..ea02290cc171d8fd5391553549be5b9e7663b1d5 GIT binary patch literal 47 vcmZ?wbhEHbWMN=oXkcLY4+e@qSr{1@7#VaJfB+=Jz{J***vNI4mBAVSN^lGu literal 0 HcmV?d00001 diff --git a/sitebricks/src/main/resources/com/google/sitebricks/core/ul.gif b/sitebricks/src/main/resources/com/google/sitebricks/core/ul.gif new file mode 100644 index 0000000000000000000000000000000000000000..9c41d5985afa5acbad641fed73325500e86c7570 GIT binary patch literal 46 ucmZ?wbhEHbWMN=oXkcLY4+e@qSr{1@7#VaJfB+=Jz{J`j&$u#%!5RQNG7D+| literal 0 HcmV?d00001 diff --git a/sitebricks/src/main/resources/com/google/sitebricks/core/ur.gif b/sitebricks/src/main/resources/com/google/sitebricks/core/ur.gif new file mode 100644 index 0000000000000000000000000000000000000000..5e27436ef4665a48c84130b401dd5bdda2a1770a GIT binary patch literal 47 vcmZ?wbhEHbWMN=oXkcLY4+e@qSr{1@7#VaJfB+=Jz{J)fn7ERMmBAVSM;8ln literal 0 HcmV?d00001 diff --git a/sitebricks/src/main/resources/com/google/sitebricks/rendering/resource/mimetypes.properties b/sitebricks/src/main/resources/com/google/sitebricks/rendering/resource/mimetypes.properties new file mode 100644 index 00000000..d795f522 --- /dev/null +++ b/sitebricks/src/main/resources/com/google/sitebricks/rendering/resource/mimetypes.properties @@ -0,0 +1,9 @@ +#mimetype patterns for static resources (matched on file name) + +#the default, if no patterns matched: +__defaultMimeType=text/plain + +#regex=mimeType +(.)*\\.js=text/javascript +(.)*\\.xml=text/xml +(.)*\\.png=image/png diff --git a/sitebricks/src/main/resources/com/google/sitebricks/templates.properties b/sitebricks/src/main/resources/com/google/sitebricks/templates.properties new file mode 100644 index 00000000..ddecd188 --- /dev/null +++ b/sitebricks/src/main/resources/com/google/sitebricks/templates.properties @@ -0,0 +1,2 @@ +warp-servlet.template.textfield= +warp-servlet.template.textarea= \ No newline at end of file diff --git a/sitebricks/src/test/java/com/google/sitebricks/EdslTest.java b/sitebricks/src/test/java/com/google/sitebricks/EdslTest.java new file mode 100644 index 00000000..000e5f3a --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/EdslTest.java @@ -0,0 +1,70 @@ +package com.google.sitebricks; + +import com.google.inject.Guice; +import com.google.inject.Scopes; +import com.google.inject.servlet.RequestScoped; +import com.google.sitebricks.headless.Reply; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.routing.Action; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class EdslTest { + + @Test + public final void edsl() { + + Guice.createInjector(new SitebricksModule() { + + @Override + protected void configureSitebricks() { + + // Registration of page classes + at("/rpc") + .show(EdslTest.class) + .asEagerSingleton(); + + at("/pub") + .show(EdslTest.class) + .in(Scopes.NO_SCOPE); + + // Registration of static resources (bundled in jar) + at("/script.js").export("/client/my.js"); + + // Register a custom SPI action. + at("/stuff") + .perform(new DummyAction()) + .on(Get.class, Post.class); + + at("/stuff/:thing") + .perform(new DummyAction()) + .select("metadata", "form") + .selectHeader("Accept", "image/jpeg") + .on(Get.class); + + // Registration of embeddable widgets (simply points to page class) + embed(EdslTest.class).as("@Blasphemy"); + embed(EdslTest.class).as("@Hiberty"); + embed(EdslTest.class).as("@Plurality").in(RequestScoped.class); + } + }); + } + + private static class DummyAction implements Action { + @Override + public boolean shouldCall(HttpServletRequest request) { + return true; + } + + @Override + public Object call(Object page, Map map) { + return Reply.saying().ok(); + } + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/LocalizationTest.java b/sitebricks/src/test/java/com/google/sitebricks/LocalizationTest.java new file mode 100644 index 00000000..5bfbb2ba --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/LocalizationTest.java @@ -0,0 +1,289 @@ +package com.google.sitebricks; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.inject.AbstractModule; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Provider; +import com.google.inject.name.Named; +import com.google.sitebricks.i18n.Message; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import static org.easymock.EasyMock.createNiceMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +/** + * Unit test for the i18n binder utility. + */ +public class LocalizationTest { + private static final String HELLO = "hello"; + private HttpServletRequest requestMock; + + @BeforeMethod + public final void setup() { + requestMock = createNiceMock(HttpServletRequest.class); + + expect(requestMock.getLocale()).andReturn(Locale.ENGLISH); + + replay(requestMock); + } + + @Test + public final void simpleLocalize() { + + final Map resourceBundle = Maps.newHashMap(); + resourceBundle.put(HELLO, "hello there!"); + + String msg = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + Set locs = Sets.newHashSet(); + locs.add(new Localizer.Localization(Localized.class, Locale.ENGLISH, resourceBundle)); + + Localizer.localizeAll(binder(), locs); + + bind(HttpServletRequest.class).toInstance(requestMock); + } + }).getInstance(Localized.class) + .hello(); + + assert resourceBundle.get(HELLO).equals(msg); + } + + @Test(expectedExceptions = CreationException.class) + public final void simpleLocalizeMissingEntry() { + + final Map resourceBundle = Maps.newHashMap(); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + Set locs = Sets.newHashSet(); + locs.add(new Localizer.Localization(Localized.class, Locale.ENGLISH, resourceBundle)); + + Localizer.localizeAll(binder(), locs); + bind(HttpServletRequest.class).toInstance(requestMock); + } + }); + } + + @Test(expectedExceptions = CreationException.class) + public final void simpleLocalizeMissingAnnotation() { + + final Map resourceBundle = Maps.newHashMap(); + resourceBundle.put(LocalizationTest.HELLO, "stuff"); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + Set locs = Sets.newHashSet(); + locs.add(new Localizer.Localization(LocalizedMissingAnnotation.class, Locale.ENGLISH, resourceBundle)); + + Localizer.localizeAll(binder(), locs); + bind(HttpServletRequest.class).toInstance(requestMock); + } + }).getInstance(LocalizedMissingAnnotation.class); + } + + @Test(expectedExceptions = CreationException.class) + public final void simpleLocalizeWrongReturnType() { + + final Map resourceBundle = Maps.newHashMap(); + resourceBundle.put(LocalizationTest.HELLO, "stuff"); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + + Set locs = Sets.newHashSet(); + locs.add(new Localizer.Localization(LocalizedWrongReturnType.class, Locale.ENGLISH, resourceBundle)); + + Localizer.localizeAll(binder(), locs); + bind(HttpServletRequest.class).toInstance(requestMock); + } + }).getInstance(LocalizedWrongReturnType.class); + } + + @Test(expectedExceptions = CreationException.class) + public final void parameterizedLocalizeWrongArgAnnotation() { + + final Map resourceBundle = Maps.newHashMap(); + resourceBundle.put(LocalizationTest.HELLO, "stuff"); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + + Set locs = Sets.newHashSet(); + locs.add(new Localizer.Localization(LocalizedWrongArgAnnotation.class, Locale.ENGLISH, resourceBundle)); + + Localizer.localizeAll(binder(), locs); + bind(HttpServletRequest.class).toInstance(requestMock); + } + }).getInstance(LocalizedWrongArgAnnotation.class); + } + + + @Test(expectedExceptions = CreationException.class) + public final void parameterizedLocalizeBrokenTemplate() { + + final Map resourceBundle = Maps.newHashMap(); + resourceBundle.put(LocalizationTest.HELLO, "stuff"); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + + Set locs = Sets.newHashSet(); + locs.add(new Localizer.Localization(LocalizedBrokenTemplate.class, Locale.ENGLISH, resourceBundle)); + + Localizer.localizeAll(binder(), locs); + bind(HttpServletRequest.class).toInstance(requestMock); + } + }).getInstance(LocalizedBrokenTemplate.class); + } + + @Test + public final void parameterizedLocalizeTemplate() { + + final Map resourceBundle = Maps.newHashMap(); + resourceBundle.put(LocalizationTest.HELLO, "hello ${name}"); + + String msg = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + + Set locs = Sets.newHashSet(); + locs.add(new Localizer.Localization(LocalizedTemplate.class, Locale.ENGLISH, resourceBundle)); + + Localizer.localizeAll(binder(), locs); + bind(HttpServletRequest.class).toInstance(requestMock); + } + }).getInstance(LocalizedTemplate.class) + .hello("Dude"); + + assert "hello Dude".equals(msg); + } + + + @Test + public final void parameterizedLocalizeTemplateMultipleLocales() { + Locale.setDefault(Locale.ENGLISH); + + final Map resourceBundle = Maps.newHashMap(); + resourceBundle.put(LocalizationTest.HELLO, "hello ${name}"); + + final HashMap japaneseBundle = Maps.newHashMap(); + japaneseBundle.put(LocalizationTest.HELLO, "konichiwa ${name}"); + + // Simulate an Accept-Language of Japanese + HttpServletRequest japaneseRequest = createNiceMock(HttpServletRequest.class); + expect(japaneseRequest.getLocale()).andReturn(Locale.JAPANESE); + replay(japaneseRequest); + + final AtomicReference mockToUse + = new AtomicReference(japaneseRequest); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + Set locs = Sets.newHashSet(); + locs.add(new Localizer.Localization(LocalizedTemplate.class, Locale.ENGLISH, resourceBundle)); + locs.add(new Localizer.Localization(LocalizedTemplate.class, Locale.JAPANESE, japaneseBundle)); + + Localizer.localizeAll(binder(), locs); + bind(HttpServletRequest.class).toProvider(new Provider() { + public HttpServletRequest get() { + return mockToUse.get(); + } + }); + } + }); + + String msg = injector.getInstance(LocalizedTemplate.class).hello("Dude"); + assert "konichiwa Dude".equals(msg) : msg; + + verify(japaneseRequest); + + // Now let's simulate english. + mockToUse.set(requestMock); + msg = injector.getInstance(LocalizedTemplate.class).hello("Dude"); + assert "hello Dude".equals(msg); + + + // Now let's simulate a totally different locale (should default to english). + // Simulate an Accept-Language of French + HttpServletRequest frenchRequest = createNiceMock(HttpServletRequest.class); + expect(frenchRequest.getLocale()).andReturn(Locale.FRENCH); + replay(frenchRequest); + + mockToUse.set(frenchRequest); + + // Assert that it uses the english locale (set as default above) + msg = injector.getInstance(LocalizedTemplate.class).hello("Dude"); + assert "hello Dude".equals(msg); + + verify(frenchRequest, requestMock); + } + + + @Test + public final void parameterizedWithNoExternalBundle() { + String msg = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + Set locs = Sets.newHashSet(); + locs.add(Localizer.defaultLocalizationFor(LocalizedTemplate.class)); + + Localizer.localizeAll(binder(), locs); + + bind(HttpServletRequest.class).toInstance(requestMock); + } + }).getInstance(LocalizedTemplate.class) + .hello("Dudette"); + + assert "hello Dudette!".equals(msg); + } + + + public static interface Localized { + @Message(message = "hello world!") + String hello(); + } + + public static interface LocalizedMissingAnnotation { + String hello(); + } + + public static interface LocalizedWrongReturnType { + @Message(message = "hello world!") + void hello(); + } + + public static interface LocalizedWrongArgAnnotation { + @Message(message = "hello world!") + String hello(String val); + } + + public static interface LocalizedBrokenTemplate { + @Message(message = "hello ${named}!") + String hello(@Named("name") String val); + } + + public static interface LocalizedTemplate { + @Message(message = "hello ${name}!") + String hello(@Named("name") String val); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/RespondTest.java b/sitebricks/src/test/java/com/google/sitebricks/RespondTest.java new file mode 100644 index 00000000..73a2520f --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/RespondTest.java @@ -0,0 +1,30 @@ +package com.google.sitebricks; + +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class RespondTest { + private static final String A_STRING = "aoskpoaksdas"; + private static final char A_CHAR = 'h'; + + @Test + public final void respondWriteStringCharAndChew() { + final Respond respond = new StringBuilderRespond(); + respond.write(A_STRING); + respond.write(A_CHAR); + respond.write(A_CHAR); + respond.chew(); + + assert (A_STRING + A_CHAR).equals(respond.toString()); + } + + @Test + public final void respondWriteNull() { + final Respond respond = new StringBuilderRespond(); + respond.write(null); + + assert ("" + null).equals(respond.toString()); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/RespondersForTesting.java b/sitebricks/src/test/java/com/google/sitebricks/RespondersForTesting.java new file mode 100644 index 00000000..e010ac11 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/RespondersForTesting.java @@ -0,0 +1,13 @@ +package com.google.sitebricks; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class RespondersForTesting { + private RespondersForTesting() { + } + + public static Respond newRespond() { + return new StringBuilderRespond(); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/TemplateLoaderTest.java b/sitebricks/src/test/java/com/google/sitebricks/TemplateLoaderTest.java new file mode 100644 index 00000000..5a6195a4 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/TemplateLoaderTest.java @@ -0,0 +1,83 @@ +package com.google.sitebricks; + +import com.google.inject.Provider; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.servlet.ServletContext; + + +import static org.easymock.EasyMock.*; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class TemplateLoaderTest { + private static final String CLASSES_AND_TEMPLATES = "classesAndTemplates"; + + @DataProvider(name = CLASSES_AND_TEMPLATES) + public Object[][] get() { + return new Object[][] { + { MyXmlPage.class }, + { My.class }, + { MyXhtml.class }, + { MyHtml.class }, + }; + } + + @Test(dataProvider = CLASSES_AND_TEMPLATES) + public final void loadExplicitXmlTemplate(final Class pageClass) { + String template = new TemplateLoader(null) + .load(pageClass).getText(); + + assert null != template : "no template found!"; + template = template.trim(); + assert template.startsWith("") && template.endsWith(""); //a weak sauce test + } + + + @Test + public void testItShouldLoadShowValueFromWebInf() { + ServletContext ctx = createMock(ServletContext.class); + + // we are telling that WEB-INF folder contains MetaInfPage.html + String realPath = TemplateLoaderTest.class.getResource("My.xml").getPath(); + + + expect(ctx.getRealPath("MetaInfPage.html")).andReturn("unknown"); + expect(ctx.getRealPath("MyMetaInfPage.html")).andReturn("unknown"); + expect(ctx.getRealPath("/WEB-INF/MyMetaInfPage.html")).andReturn("unknown"); + expect(ctx.getRealPath("/WEB-INF/MetaInfPage.html")).andReturn(realPath); + + replay(ctx); + String template = new TemplateLoader(new MockServletContextProvider(ctx)).load(MyMetaInfPage.class).getText(); + verify(ctx); + + assert null != template : "no template found!"; + assert template.contains("hello") : "template was not loaded correctly?"; + } + + @Show("MetaInfPage.html") + public static class MyMetaInfPage { } + + @Show("My.xml") + public static class MyXmlPage { } + + + + public static class My { } + public static class MyXhtml { } + public static class MyHtml { } + + class MockServletContextProvider implements Provider { + private final ServletContext ctx; + + public MockServletContextProvider(ServletContext ctx) { + this.ctx = ctx; + } + + public ServletContext get() { + return ctx; + } + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/WidgetFilterTest.java b/sitebricks/src/test/java/com/google/sitebricks/WidgetFilterTest.java new file mode 100644 index 00000000..67136e24 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/WidgetFilterTest.java @@ -0,0 +1,156 @@ +package com.google.sitebricks; + +import com.google.inject.util.Providers; +import com.google.sitebricks.routing.RoutingDispatcher; +import org.testng.annotations.Test; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Date; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.createNiceMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class WidgetFilterTest { + private static final String SOME_OUTPUT = "some outputdaoskdasd__" + new Date(); + private static final String A_REDIRECT_LOCATION = "/a/redirect/location"; + private static final String TEXT_HTML = "text/html"; + + @Test + public final void init() throws ServletException { + final FilterConfig filterConfig = createMock(FilterConfig.class); + final Bootstrapper bootstrapper = createMock(Bootstrapper.class); + + bootstrapper.start(); + + replay(bootstrapper); + + new SitebricksFilter(createNiceMock(RoutingDispatcher.class), Providers.of(bootstrapper), + Providers.of(null)).init(filterConfig); + + + verify(bootstrapper); + } + + @Test + public final void doFilter() throws IOException, ServletException { + RoutingDispatcher dispatcher = createMock(RoutingDispatcher.class); + HttpServletRequest request = createNiceMock(HttpServletRequest.class); + HttpServletResponse response = createMock(HttpServletResponse.class); + FilterChain filterChain = createMock(FilterChain.class); + Respond respond = new StringBuilderRespond() { + @Override + public String toString() { + return SOME_OUTPUT; + } + + @Override + public String getRedirect() { + return null; + } + + @Override + public String getContentType() { + return TEXT_HTML; + } + }; + + expect(dispatcher.dispatch(request, response)) + .andReturn(respond); + + //nothing set? + expect(response.getContentType()) + .andReturn(null); + + response.setContentType(TEXT_HTML); + + final boolean[] outOk = new boolean[2]; + final String[] output = new String[1]; + expect(response.getWriter()) + .andReturn(new PrintWriter(System.out) { + @Override + public void write(String s) { + outOk[0] = true; + output[0] = s; + } + + @Override + public void flush() { + outOk[1] = true; + } + }); + + replay(dispatcher, request, response, filterChain); + + new SitebricksFilter(dispatcher, Providers.of(null), + Providers.of(null)).doFilter(request, response, filterChain); + + assert outOk[0] && !outOk[1] : "Response not written or flushed correctly"; + assert SOME_OUTPUT.equals(output[0]) : "Respond output not used"; + verify(dispatcher, request, response, filterChain); + } + + + @Test + public final void doFilterDoesntHandleButProceedsDownChain() throws IOException, ServletException { + RoutingDispatcher dispatcher = createMock(RoutingDispatcher.class); + HttpServletRequest request = createMock(HttpServletRequest.class); + HttpServletResponse response = createMock(HttpServletResponse.class); + FilterChain filterChain = createMock(FilterChain.class); + + //meaning no dispatch could be performed... + expect(dispatcher.dispatch(request, response)) + .andReturn(null); + + filterChain.doFilter(request, response); + expectLastCall().once(); + + replay(dispatcher, request, response, filterChain); + + new SitebricksFilter(dispatcher, Providers.of(null), + Providers.of(null)) + .doFilter(request, response, filterChain); + + verify(dispatcher, request, response, filterChain); + } + + @Test + public final void doFilterRedirects() throws IOException, ServletException { + RoutingDispatcher dispatcher = createMock(RoutingDispatcher.class); + HttpServletRequest request = createNiceMock(HttpServletRequest.class); + HttpServletResponse response = createMock(HttpServletResponse.class); + FilterChain filterChain = createMock(FilterChain.class); + + Respond respond = createMock(Respond.class); + + //meaning no dispatch could be performed... + expect(dispatcher.dispatch(request, response)) + .andReturn(respond); + + expect(respond.getRedirect()) + .andReturn(A_REDIRECT_LOCATION); + + response.sendRedirect(A_REDIRECT_LOCATION); + + replay(dispatcher, request, response, filterChain, respond); + + new SitebricksFilter(dispatcher, Providers.of(null), + Providers.of(null)) + .doFilter(request, response, filterChain); + + verify(dispatcher, request, response, filterChain, respond); + } + +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/binding/MvelRequestBinderTest.java b/sitebricks/src/test/java/com/google/sitebricks/binding/MvelRequestBinderTest.java new file mode 100644 index 00000000..b7bc4457 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/binding/MvelRequestBinderTest.java @@ -0,0 +1,214 @@ +package com.google.sitebricks.binding; + +import com.google.inject.Guice; +import com.google.inject.Provider; +import com.google.sitebricks.Evaluator; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.HashMap; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class MvelRequestBinderTest { + @Test + public final void bindRequestToPrimitives() { + final HttpServletRequest request = createMock(HttpServletRequest.class); + + expect(request.getParameterMap()) + .andReturn(new HashMap() {{ + put("name", new String[]{"Dhanji"}); + put("age", new String[]{"27"}); + put("alive", new String[]{"true"}); + put("id", new String[]{"12"}); + put("height", new String[]{"6.0"}); + }}); + + replay(request); + + final AnObject o = new AnObject(); + + final Evaluator evaluator = Guice.createInjector() + .getInstance(Evaluator.class); + + new MvelRequestBinder(evaluator, new Provider() { + public FlashCache get() { + return new HttpSessionFlashCache(); + } + }) + .bind(request, o); + + assert "Dhanji".equals(o.getName()); + assert 27 == (o.getAge()); + assert 12L == (o.getId()); + assert 6.0 == (o.getHeight()); + assert (o.isAlive()); + + verify(request); + } + + @Test + public final void bindRequestToCollections() { + final HttpServletRequest request = createMock(HttpServletRequest.class); + final String choice = "AChoice"; + + //setup preliminary request + final HttpSessionFlashCache cache = new HttpSessionFlashCache(); + cache.put("names", Arrays.asList("First", choice, "BobLee", "JasonLee", "Mowglee")); + + expect(request.getParameterMap()) + .andReturn(new HashMap() {{ + put("select", + new String[]{RequestBinder.COLLECTION_BIND_PREFIX + "names/" + choice.hashCode()}); + }}); + + replay(request); + + final AnObject o = new AnObject(); + + final Evaluator evaluator = Guice.createInjector() + .getInstance(Evaluator.class); + + new MvelRequestBinder(evaluator, new Provider() { + public FlashCache get() { + return cache; + } + }) + .bind(request, o); + + assert choice.equals(o.getSelect()) : "Collection selectee was not bound: " + o.getSelect(); + verify(request); + } + + @Test + public final void bindRequestToPrimitivesAndIgnoreExtras() { + final HttpServletRequest request = createMock(HttpServletRequest.class); + + expect(request.getParameterMap()) + .andReturn(new HashMap() {{ + put("name", new String[]{"Dhanji"}); + put("age", new String[]{"27"}); + put("alive", new String[]{"true"}); + put("id", new String[]{"12"}); + put("height", new String[]{"6.0"}); + put("weight", new String[]{"6.0"}); + put("hiphop", new String[]{"6.0"}); + }}); + + replay(request); + + final AnObject o = new AnObject(); + + final Evaluator evaluator = Guice.createInjector() + .getInstance(Evaluator.class); + + new MvelRequestBinder(evaluator, new Provider() { + public FlashCache get() { + return new HttpSessionFlashCache(); + } + }) + .bind(request, o); + + assert "Dhanji".equals(o.getName()); + assert 27 == (o.getAge()); + assert 12L == (o.getId()); + assert 6.0 == (o.getHeight()); + assert (o.isAlive()); + + verify(request); + } + + @Test(expectedExceptions = InvalidBindingException.class) + public final void bindRequestDetectInvalid() { + final HttpServletRequest request = createMock(HttpServletRequest.class); + + expect(request.getParameterMap()) + .andReturn(new HashMap() {{ + put("name.toString()", new String[]{"Dhanji"}); + put("2 + 12", new String[]{"27"}); + put("#@!*^&", new String[]{"true"}); + put("id", new String[]{"12"}); + put("height", new String[]{"6.0"}); + }}); + + replay(request); + + final AnObject o = new AnObject(); + + final Evaluator evaluator = Guice.createInjector() + .getInstance(Evaluator.class); + + new MvelRequestBinder(evaluator, new Provider() { + public FlashCache get() { + return new HttpSessionFlashCache(); + } + }) + .bind(request, o); + + } + + @SuppressWarnings({"UnusedDeclaration"}) + public static class AnObject { + private String name; + private int age; + private boolean alive; + private Long id; + private double height; + private String select; + + public String getSelect() { + return select; + } + + public void setSelect(String select) { + this.select = select; + } + + public double getHeight() { + return height; + } + + public void setHeight(double height) { + this.height = height; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public boolean isAlive() { + return alive; + } + + public void setAlive(boolean alive) { + this.alive = alive; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/binding/PropertyCacheTest.java b/sitebricks/src/test/java/com/google/sitebricks/binding/PropertyCacheTest.java new file mode 100644 index 00000000..4e09934b --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/binding/PropertyCacheTest.java @@ -0,0 +1,21 @@ +package com.google.sitebricks.binding; + +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class PropertyCacheTest { + + @Test + public final void doesPropertyExist() { + assert new ConcurrentPropertyCache() + .exists("name", MvelRequestBinderTest.AnObject.class); + + assert new ConcurrentPropertyCache() + .exists("name", MvelRequestBinderTest.AnObject.class); + + assert !new ConcurrentPropertyCache() + .exists("notExists", MvelRequestBinderTest.AnObject.class); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/compiler/FreemarkerTemplateCompilerTest.java b/sitebricks/src/test/java/com/google/sitebricks/compiler/FreemarkerTemplateCompilerTest.java new file mode 100644 index 00000000..ba5bde48 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/compiler/FreemarkerTemplateCompilerTest.java @@ -0,0 +1,422 @@ +package com.google.sitebricks.compiler; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.createNiceMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.testng.Assert.assertEquals; + +import java.lang.annotation.Annotation; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import com.google.common.collect.Maps; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.sitebricks.Bricks; +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.MvelEvaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.RespondersForTesting; +import com.google.sitebricks.compiler.template.freemarker.FreemarkerTemplateCompiler; +import com.google.sitebricks.http.Delete; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.http.Put; +import com.google.sitebricks.rendering.EmbedAs; +import com.google.sitebricks.rendering.control.Chains; +import com.google.sitebricks.rendering.control.WidgetRegistry; +import com.google.sitebricks.routing.PageBook; +import com.google.sitebricks.routing.SystemMetrics; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class FreemarkerTemplateCompilerTest { + private static final String ANNOTATION_EXPRESSIONS = "Annotation expressions"; + private Injector injector; + private PageBook pageBook; + private SystemMetrics metrics; + private final Map> methods = Maps.newHashMap(); + + @BeforeMethod + public void pre() { + methods.put("get", Get.class); + methods.put("post", Post.class); + methods.put("put", Put.class); + methods.put("delete", Delete.class); + + injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + bind(new TypeLiteral>>() { + }) + .annotatedWith(Bricks.class) + .toInstance(methods); + } + }); + + pageBook = createNiceMock(PageBook.class); + metrics = createNiceMock(SystemMetrics.class); + } + + @Test + public final void annotationKeyExtraction() { + assert "link".equals(Dom.extractKeyAndContent("@Link")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing()")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing(asodkoas)")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing(asodkoas) ")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing(asodkoas) kko")[0]) : "Extraction wrong: "; + + assert "".equals(Dom.extractKeyAndContent("@Link")[1]) : "Extraction wrong: "; + final String val = Dom.extractKeyAndContent("@Thing()")[1]; + assert null == (val) : "Extraction wrong: " + val; + assert "asodkoas".equals(Dom.extractKeyAndContent("@Thing(asodkoas)")[1]) : "Extraction wrong: "; + assert "asodkoas".equals(Dom.extractKeyAndContent("@Thing(asodkoas) ")[1]) : "Extraction wrong: "; + assert "asodkoas".equals(Dom.extractKeyAndContent("@Thing(asodkoas) kko")[1]) : "Extraction wrong: "; + } + + @Test + public final void readShowIfWidgetTrue() { + + Renderable widget = + new FreemarkerTemplateCompiler(Object.class) + .compile("<#if true>

hello

"); + + assert null != widget : " null "; + + final StringBuilder builder = new StringBuilder(); + final Respond mockRespond = RespondersForTesting.newRespond(); + widget.render(new Object(), mockRespond); + final String value = mockRespond.toString(); + System.out.println(value); + assert "

hello

".equals(value) : "Did not write expected output, instead: " + value; + } + + + @DataProvider(name = ANNOTATION_EXPRESSIONS) + public Object[][] get() { + return new Object[][]{ + {"true"}, +// {"java.lang.Boolean.TRUE"}, +// {"java.lang.Boolean.valueOf('true')"}, +// {"true ? true : true"}, @TODO (BD): Disabled until I actually investigate if this is a valid test. + {"'x' == 'x'"}, + {"\"x\" == \"x\""}, +// {"'hello' instanceof java.io.Serializable"}, +// {"true; return true"}, +// {" 5 >= 2 "}, + }; + } + + @Test(dataProvider = ANNOTATION_EXPRESSIONS) + public final void readAWidgetWithVariousExpressions(String expression) { + final Evaluator evaluator = new MvelEvaluator(); + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + String templateValue = String.format("<#if %s>

hello

", expression); + + System.out.println( templateValue ); + + Renderable widget = + new FreemarkerTemplateCompiler(Object.class) + .compile(templateValue); + + assert null != widget : " null "; + + final StringBuilder builder = new StringBuilder(); + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new Object(), mockRespond); + + final String value = mockRespond.toString(); + System.out.println(value); + assert "

hello

".equals(value) : "Did not write expected output, instead: " + value; + } + + + @Test + public final void readShowIfWidgetFalse() { + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + } + }); + + final Evaluator evaluator = new MvelEvaluator(); + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + + Renderable widget = + new FreemarkerTemplateCompiler(Object.class) + .compile("<#if false>

hello

"); + + assert null != widget : " null "; + + final StringBuilder builder = new StringBuilder(); + + final Respond mockRespond = RespondersForTesting.newRespond(); + widget.render(new Object(), mockRespond); + + final String value = mockRespond.toString(); + assert "".equals(value) : "Did not write expected output, instead: " + value; + } + + + @Test + public final void readTextWidgetValues() { + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + } + }); + + Renderable widget = + new FreemarkerTemplateCompiler(Object.class) + .compile("
hello ${name}
"); + + assert null != widget : " null "; + + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String value = mockRespond.toString(); + assert "
hello Dhanji
" + .replace("\"", "'") + .equals(value) : "Did not write expected output, instead: " + value; + } + + public static class TestBackingType { + private String name; + private String clazz; + private Integer id; + + public TestBackingType(String name, String clazz, Integer id) { + this.name = name; + this.clazz = clazz; + this.id = id; + } + + public String getName() { + return name; + } + + public String getClazz() { + return clazz; + } + + public Integer getId() { + return id; + } + } + + +// @Test +// public final void readAndRenderRequireWidget() { +// final Injector injector = Guice.createInjector(new AbstractModule() { +// protected void configure() { +// bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); +// bind(new TypeLiteral>>() { +// }) +// .annotatedWith(Bricks.class) +// .toInstance(methods); +// } +// }); +// +// +// final PageBook pageBook = injector.getInstance(PageBook.class); +// +// +// final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); +// +// +// Renderable widget = +// new FreemarkerTemplateCompiler(Object.class) +// .compile(" " + +// " @Require " + +// " @Require " + +// "" + +// "
hello ${name}
" + +// ""); +// +// assert null != widget : " null "; +// +// final Respond respond = RespondersForTesting.newRespond(); +// +// widget.render(new TestBackingType("Dhanji", "content", 12), respond); +// +// final String value = respond.toString(); +// String expected = " " + +// " " + +// "" + +// "
hello Dhanji
"; +// expected = expected.replaceAll("'", "\""); +// +// assertEquals(value, expected); +// } + + + @Test + public final void readHtmlWidget() { + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + Renderable widget = + new FreemarkerTemplateCompiler(Object.class) + .compile("
hello
"); + + assert null != widget : " null "; + + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String s = mockRespond.toString(); + assert "
hello
" + .replace( "\"", "'") + .equals(s) : "Did not write expected output, instead: " + s; + } + + + @Test + public final void readHtmlWidgetWithChildren() { + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + Renderable widget = + new FreemarkerTemplateCompiler(Object.class) + .compile("
hello <#if false>hideme
"); + + assert null != widget : " null "; + + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String s = mockRespond.toString(); + assertEquals(s, "
hello
".replace("\"", "'")); + } + + @EmbedAs(MyEmbeddedPage.MY_FAVE_ANNOTATION) + public static class MyEmbeddedPage { + protected static final String MY_FAVE_ANNOTATION = "MyFave"; + private boolean should = true; + + public boolean isShould() { + return should; + } + + public void setShould(boolean should) { + this.should = should; + } + } + +// @Test +// public final void readEmbedWidgetAndStoreAsPage() { +// final Injector injector = Guice.createInjector(new AbstractModule() { +// protected void configure() { +// bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); +// bind(new TypeLiteral>>() { +// }) +// .annotatedWith(Bricks.class) +// .toInstance(methods); +// } +// }); +// final PageBook book = injector //hacky, where are you super-packages! +// .getInstance(PageBook.class); +// +// book.at("/somewhere", MyEmbeddedPage.class).apply(Chains.terminal()); +// +// +// final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); +// registry.addEmbed("myfave"); +// +// Renderable widget = +// new FreemarkerTemplateCompiler(Object.class) +// .compile("
hello @MyFave(should=false)hideme
"); +// +// assert null != widget : " null "; +// +// //tell pagebook to track this as an embedded widget +// book.embedAs(MyEmbeddedPage.class, MyEmbeddedPage.MY_FAVE_ANNOTATION) +// .apply(Chains.terminal()); +// +// final Respond mockRespond = RespondersForTesting.newRespond(); +// +// widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); +// +// final String s = mockRespond.toString(); +// assert "
hello
" +// .equals(s) : "Did not write expected output, instead: " + s; +// } + + +// @Test +// public final void readEmbedWidgetOnly() { +// final Injector injector = Guice.createInjector(new AbstractModule() { +// protected void configure() { +// bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); +// bind(new TypeLiteral>>() { +// }) +// .annotatedWith(Bricks.class) +// .toInstance(methods); +// } +// }); +// final PageBook book = injector //hacky, where are you super-packages! +// .getInstance(PageBook.class); +// +// +// final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); +// registry.addEmbed("myfave"); +// +// Renderable widget = +// new FreemarkerTemplateCompiler(Object.class) +// .compile("
hello @MyFave(should=false)hideme
"); +// +// assert null != widget : " null "; +// +// //tell pagebook to track this as an embedded widget +// book.embedAs(MyEmbeddedPage.class, MyEmbeddedPage.MY_FAVE_ANNOTATION) +// .apply(Chains.terminal()); +// +// final Respond mockRespond = RespondersForTesting.newRespond(); +// +// widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); +// +// final String s = mockRespond.toString(); +// assert "
hello
" +// .replace( "\"", "'" ) +// .equals(s) : "Did not write expected output, instead: " + s; +// } + + static Provider mockRequestProviderForContext() { + return new Provider() { + public HttpServletRequest get() { + final HttpServletRequest request = createMock(HttpServletRequest.class); + expect(request.getContextPath()) + .andReturn("") + .anyTimes(); + replay(request); + + return request; + } + }; + } + +} \ No newline at end of file diff --git a/sitebricks/src/test/java/com/google/sitebricks/compiler/HtmlTemplateCompilerTest.java b/sitebricks/src/test/java/com/google/sitebricks/compiler/HtmlTemplateCompilerTest.java new file mode 100644 index 00000000..4f84d4ef --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/compiler/HtmlTemplateCompilerTest.java @@ -0,0 +1,486 @@ +package com.google.sitebricks.compiler; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.inject.*; +import com.google.sitebricks.*; +import com.google.sitebricks.conversion.MvelTypeConverter; +import com.google.sitebricks.conversion.TypeConverter; +import com.google.sitebricks.http.Delete; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.http.Put; +import com.google.sitebricks.rendering.EmbedAs; +import com.google.sitebricks.rendering.control.Chains; +import com.google.sitebricks.rendering.control.WidgetRegistry; +import com.google.sitebricks.routing.PageBook; +import com.google.sitebricks.routing.SystemMetrics; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import java.lang.annotation.Annotation; +import java.util.Map; + +import static org.easymock.EasyMock.*; +import static org.testng.Assert.assertEquals; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class HtmlTemplateCompilerTest { + private static final String ANNOTATION_EXPRESSIONS = "Annotation expressions"; + private Injector injector; + private PageBook pageBook; + private SystemMetrics metrics; + private final Map> methods = Maps.newHashMap(); + + @BeforeMethod + public void pre() { + methods.put("get", Get.class); + methods.put("post", Post.class); + methods.put("put", Put.class); + methods.put("delete", Delete.class); + + injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + bind(new TypeLiteral>>() { + }) + .annotatedWith(Bricks.class) + .toInstance(methods); + } + }); + + pageBook = createNiceMock(PageBook.class); + metrics = createNiceMock(SystemMetrics.class); + } + + @Test + public final void annotationKeyExtraction() { + assert "link".equals(Dom.extractKeyAndContent("@Link")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing()")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing(asodkoas)")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing(asodkoas) ")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing(asodkoas) kko")[0]) : "Extraction wrong: "; + + assert "".equals(Dom.extractKeyAndContent("@Link")[1]) : "Extraction wrong: "; + final String val = Dom.extractKeyAndContent("@Thing()")[1]; + assert null == (val) : "Extraction wrong: " + val; + assert "asodkoas".equals(Dom.extractKeyAndContent("@Thing(asodkoas)")[1]) : "Extraction wrong: "; + assert "asodkoas".equals(Dom.extractKeyAndContent("@Thing(asodkoas) ")[1]) : "Extraction wrong: "; + assert "asodkoas".equals(Dom.extractKeyAndContent("@Thing(asodkoas) kko")[1]) : "Extraction wrong: "; + } + + @Test + public final void readShowIfWidgetTrue() { + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + final MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(TestBackingType.class); + Renderable widget = + new HtmlTemplateCompiler(Object.class, compiler, registry, pageBook, metrics) + .compile("@ShowIf(true)

hello

"); + +// .compile("\n" + +// "small test\n" + +// "@ShowIf(true)

hello

" + +// "\n"); + + + assert null != widget : " null "; + + final StringBuilder builder = new StringBuilder(); + final Respond mockRespond = RespondersForTesting.newRespond(); +// final Respond mockRespond = new StringBuilderRespond() { +// @Override +// public void write(String text) { +// builder.append(text); +// } +// +// @Override +// public void write(char text) { +// builder.append(text); +// } +// +// @Override +// public void chew() { +// builder.deleteCharAt(builder.length() - 1); +// } +// }; + + widget.render(new Object(), mockRespond); + + final String value = mockRespond.toString(); + System.out.println(value); + assert "

hello

".equals(value) : "Did not write expected output, instead: " + value; + // assert "small test

hello

".equals(value) : "Did not write expected output, instead: " + value; + } + + + @DataProvider(name = ANNOTATION_EXPRESSIONS) + public Object[][] get() { + return new Object[][]{ + {"true"}, + {"java.lang.Boolean.TRUE"}, + {"java.lang.Boolean.valueOf('true')"}, +// {"true ? true : true"}, @TODO (BD): Disabled until I actually investigate if this is a valid test. + {"'x' == 'x'"}, + {"\"x\" == \"x\""}, + {"'hello' instanceof java.io.Serializable"}, + {"true; return true"}, + {" 5 >= 2 "}, + }; + } + + @Test(dataProvider = ANNOTATION_EXPRESSIONS) + public final void readAWidgetWithVariousExpressions(String expression) { + final Evaluator evaluator = new MvelEvaluator(); + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + + Renderable widget = + new HtmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(Object.class), registry, pageBook, metrics) + .compile(String.format("@ShowIf(%s)

hello

", expression)); + + assert null != widget : " null "; + + final StringBuilder builder = new StringBuilder(); + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new Object(), mockRespond); + + final String value = mockRespond.toString(); + System.out.println(value); + assert "

hello

".equals(value) : "Did not write expected output, instead: " + value; + } + + + @Test + public final void readShowIfWidgetFalse() { + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + } + }); + + final Evaluator evaluator = new MvelEvaluator(); + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + + Renderable widget = + new HtmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(Object.class), registry, pageBook, metrics) + .compile("@ShowIf(false)

hello

"); + + assert null != widget : " null "; + + final StringBuilder builder = new StringBuilder(); + + final Respond mockRespond = RespondersForTesting.newRespond(); + widget.render(new Object(), mockRespond); + + final String value = mockRespond.toString(); + assert "".equals(value) : "Did not write expected output, instead: " + value; + } + + + @Test + public final void readTextWidgetValues() { + final Evaluator evaluator = new MvelEvaluator(); + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + } + }); + + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + + Renderable widget = + new HtmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, pageBook, metrics) + .compile("
hello ${name}
"); + + assert null != widget : " null "; + + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String value = mockRespond.toString(); + assert "
hello Dhanji
" + .replaceAll("'", "\"") + .equals(value) : "Did not write expected output, instead: " + value; + } + + public static class TestBackingType { + private String name; + private String clazz; + private Integer id; + + public TestBackingType(String name, String clazz, Integer id) { + this.name = name; + this.clazz = clazz; + this.id = id; + } + + public String getName() { + return name; + } + + public String getClazz() { + return clazz; + } + + public Integer getId() { + return id; + } + } + + + @Test + public final void readAndRenderRequireWidget() { + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + bind(new TypeLiteral>>() { + }) + .annotatedWith(Bricks.class) + .toInstance(methods); + } + }); + + // make a basic type converter without creating + TypeConverter converter = new MvelTypeConverter(); + Parsing.setTypeConverter(converter); + + final PageBook pageBook = injector.getInstance(PageBook.class); + + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + + Renderable widget = + new HtmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, pageBook, metrics) + .compile(" " + + " @Require " + + " @Require " + + "" + + "
hello ${name}
" + + ""); + + assert null != widget : " null "; + + final Respond respond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), respond); + + final String value = respond.toString(); + String expected = " " + + " " + + "" + + "
hello Dhanji
"; + expected = expected.replaceAll("'", "\""); + + assertEquals(value, expected); + } + + + @Test + public final void readHtmlWidget() { + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + Renderable widget = + new HtmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, pageBook, metrics) + .compile("
hello
"); + + assert null != widget : " null "; + + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String s = mockRespond.toString(); + assert "
hello
" + .equals(s) : "Did not write expected output, instead: " + s; + } + + + @Test + public final void readHtmlWidgetWithChildren() { + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + Renderable widget = + new HtmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, pageBook, metrics) + .compile("
hello @ShowIf(false)hideme
"); + + assert null != widget : " null "; + + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String s = mockRespond.toString(); + assertEquals(s, "
hello
"); + } + + @EmbedAs(MyEmbeddedPage.MY_FAVE_ANNOTATION) + public static class MyEmbeddedPage { + protected static final String MY_FAVE_ANNOTATION = "MyFave"; + private boolean should = true; + + public boolean isShould() { + return should; + } + + public void setShould(boolean should) { + this.should = should; + } + } + + @Test + public final void readEmbedWidgetAndStoreAsPage() { + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + bind(new TypeLiteral>>() { + }) + .annotatedWith(Bricks.class) + .toInstance(methods); + } + }); + final PageBook book = injector //hacky, where are you super-packages! + .getInstance(PageBook.class); + + book.at("/somewhere", MyEmbeddedPage.class).apply(Chains.terminal()); + + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + registry.addEmbed("myfave"); + + Renderable widget = + new HtmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, book, metrics) + .compile("
hello @MyFave(should=false)hideme
"); + + assert null != widget : " null "; + + //tell pagebook to track this as an embedded widget + book.embedAs(MyEmbeddedPage.class, MyEmbeddedPage.MY_FAVE_ANNOTATION) + .apply(Chains.terminal()); + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String s = mockRespond.toString(); + assert "
hello
" + .equals(s) : "Did not write expected output, instead: " + s; + } + + + @Test + public final void readEmbedWidgetOnly() { + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + bind(new TypeLiteral>>() { + }) + .annotatedWith(Bricks.class) + .toInstance(methods); + } + }); + final PageBook book = injector //hacky, where are you super-packages! + .getInstance(PageBook.class); + + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + registry.addEmbed("myfave"); + + Renderable widget = + new HtmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, pageBook, metrics) + .compile("
hello @MyFave(should=false)hideme
"); + + assert null != widget : " null "; + + //tell pagebook to track this as an embedded widget + book.embedAs(MyEmbeddedPage.class, MyEmbeddedPage.MY_FAVE_ANNOTATION) + .apply(Chains.terminal()); + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String s = mockRespond.toString(); + assert "
hello
" + .equals(s) : "Did not write expected output, instead: " + s; + } + + + //TODO Fix this test! +// @Test +// public final void readEmbedWidgetWithArgs() throws ExpressionCompileException { +// +// final Evaluator evaluator = new MvelEvaluator(); +// final Injector injector = Guice.createInjector(new AbstractModule() { +// protected void configure() { +// bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); +// } +// }); +// final PageBook book = injector.getInstance(PageBook.class); //hacky, where are you super-packages! +// +// final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); +// +// final MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(TestBackingType.class); +// Renderable widget = +// new HtmlTemplateCompiler(Object.class, compiler, registry, book, metrics) +// .compile("
hello @MyFave(should=true) @With(\"me\")

showme

"); +// +// assert null != widget : " null "; +// +// +// HtmlWidget bodyWrapper = new XmlWidget(Chains.proceeding().addWidget(new IncludeWidget(new TerminalWidgetChain(), "'me'", evaluator)), +// "body", compiler, Collections.emptyMap()); +// +// bodyWrapper.setRequestProvider(mockRequestProviderForContext()); +// +// //should include the @With("me") annotated widget from the template above (discarding the

tag). +// book.embedAs(MyEmbeddedPage.class).apply(bodyWrapper); +// +// final Respond mockRespond = new StringBuilderRespond(); +// +// widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); +// +// final String s = mockRespond.toString(); +// assert "

hello showme
" +// .equals(s) : "Did not write expected output, instead: " + s; +// } + + public static Provider mockRequestProviderForContext() { + return new Provider() { + public HttpServletRequest get() { + final HttpServletRequest request = createMock(HttpServletRequest.class); + expect(request.getContextPath()) + .andReturn("") + .anyTimes(); + expect(request.getMethod()) + .andReturn("POST") + .anyTimes(); + expect(request.getParameterMap()) + .andReturn(ImmutableMap.of()) + .anyTimes(); + replay(request); + + return request; + } + }; + } + +} \ No newline at end of file diff --git a/sitebricks/src/test/java/com/google/sitebricks/compiler/XmlLineNumberParsingTest.java b/sitebricks/src/test/java/com/google/sitebricks/compiler/XmlLineNumberParsingTest.java new file mode 100644 index 00000000..4aee9b3f --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/compiler/XmlLineNumberParsingTest.java @@ -0,0 +1,45 @@ +package com.google.sitebricks.compiler; + +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; +import org.testng.annotations.Test; + +import java.io.StringReader; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class XmlLineNumberParsingTest { + private static final String XML = "\n\n\n\n\n\n\n" + + " helo\n\n" + + " " + + "\n"; + + private static final String FAULTY_XML = "\n\n\n\n\n\n\n" + + " ${broken}\n\n" + + " " + + "\n"; + + @Test + public final void filterParsesLineNumbersIntoAttribute() throws DocumentException { + + final SAXReader reader = new SAXReader(); + reader.setXMLFilter(Dom.newLineNumberFilter()); + + final Document document = reader.read(new StringReader(XML)); + + assert 1 == lineNumberOf(document, "/xml"); + assert 8 == lineNumberOf(document, "/xml/node"); + assert 10 == lineNumberOf(document, "/xml/dod"); + + } + + private static int lineNumberOf(Document document, final String xpath) { + return Integer.parseInt((((Element) document.selectSingleNode(xpath))) + .attribute(Dom.LINE_NUMBER_ATTRIBUTE) + .getValue() + ); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/compiler/XmlTemplateCompilerTest.java b/sitebricks/src/test/java/com/google/sitebricks/compiler/XmlTemplateCompilerTest.java new file mode 100644 index 00000000..a1b919f5 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/compiler/XmlTemplateCompilerTest.java @@ -0,0 +1,459 @@ +package com.google.sitebricks.compiler; + +import com.google.common.collect.Maps; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.TypeLiteral; +import com.google.sitebricks.*; +import com.google.sitebricks.http.Delete; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.http.Put; +import com.google.sitebricks.rendering.EmbedAs; +import com.google.sitebricks.rendering.control.Chains; +import com.google.sitebricks.rendering.control.WidgetRegistry; +import com.google.sitebricks.routing.PageBook; +import com.google.sitebricks.routing.SystemMetrics; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import java.lang.annotation.Annotation; +import java.util.Map; + +import static com.google.sitebricks.compiler.HtmlTemplateCompilerTest.mockRequestProviderForContext; +import static org.easymock.EasyMock.createNiceMock; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class XmlTemplateCompilerTest { + private static final String ANNOTATION_EXPRESSIONS = "Annotation expressions"; + private Injector injector; + private PageBook pageBook; + private SystemMetrics metrics; + private final Map> methods = Maps.newHashMap(); + + @BeforeMethod + public void pre() { + methods.put("get", Get.class); + methods.put("post", Post.class); + methods.put("put", Put.class); + methods.put("delete", Delete.class); + + injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + bind(new TypeLiteral>>() { + }) + .annotatedWith(Bricks.class) + .toInstance(methods); + } + }); + + pageBook = createNiceMock(PageBook.class); + metrics = createNiceMock(SystemMetrics.class); + } + + @Test + public final void annotationKeyExtraction() { + assert "link".equals(Dom.extractKeyAndContent("@Link")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing()")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing(asodkoas)")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing(asodkoas) ")[0]) : "Extraction wrong: "; + assert "thing".equals(Dom.extractKeyAndContent("@Thing(asodkoas) kko")[0]) : "Extraction wrong: "; + + assert "".equals(Dom.extractKeyAndContent("@Link")[1]) : "Extraction wrong: "; + final String val = Dom.extractKeyAndContent("@Thing()")[1]; + assert null == (val) : "Extraction wrong: " + val; + assert "asodkoas".equals(Dom.extractKeyAndContent("@Thing(asodkoas)")[1]) : "Extraction wrong: "; + assert "asodkoas".equals(Dom.extractKeyAndContent("@Thing(asodkoas) ")[1]) : "Extraction wrong: "; + assert "asodkoas".equals(Dom.extractKeyAndContent("@Thing(asodkoas) kko")[1]) : "Extraction wrong: "; + } + + @Test + public final void readShowIfWidgetTrue() { + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + final MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(TestBackingType.class); + Renderable widget = + new XmlTemplateCompiler(Object.class, compiler, registry, pageBook, metrics) + .compile("@ShowIf(true)

hello

"); + + assert null != widget : " null "; + + final StringBuilder builder = new StringBuilder(); + final Respond mockRespond = RespondersForTesting.newRespond(); +// final Respond mockRespond = new StringBuilderRespond() { +// @Override +// public void write(String text) { +// builder.append(text); +// } +// +// @Override +// public void write(char text) { +// builder.append(text); +// } +// +// @Override +// public void chew() { +// builder.deleteCharAt(builder.length() - 1); +// } +// }; + + widget.render(new Object(), mockRespond); + + final String value = mockRespond.toString(); +// System.out.println(value); + assert "

hello

".equals(value) : "Did not write expected output, instead: " + value; + } + + + @DataProvider(name = ANNOTATION_EXPRESSIONS) + public Object[][] get() { + return new Object[][]{ + {"true"}, + {"java.lang.Boolean.TRUE"}, + {"java.lang.Boolean.valueOf('true')"}, +// {"true ? true : true"}, @TODO (BD): Disabled until I actually investigate if this is a valid test. + {"'x' == 'x'"}, + {"\"x\" == \"x\""}, + {"'hello' instanceof java.io.Serializable"}, + {"true; return true"}, + {" 5 >= 2 "}, + }; + } + + @Test(dataProvider = ANNOTATION_EXPRESSIONS) + public final void readAWidgetWithVariousExpressions(String expression) { + final Evaluator evaluator = new MvelEvaluator(); + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + + Renderable widget = + new XmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(Object.class), registry, pageBook, metrics) + .compile(String.format("@ShowIf(%s)

hello

", expression)); + + assert null != widget : " null "; + + final StringBuilder builder = new StringBuilder(); + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new Object(), mockRespond); + + final String value = mockRespond.toString(); +// System.out.println(value); + assert "

hello

".equals(value) : "Did not write expected output, instead: " + value; + } + + + @Test + public final void readShowIfWidgetFalse() { + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + } + }); + + final Evaluator evaluator = new MvelEvaluator(); + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + + Renderable widget = + new XmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(Object.class), registry, pageBook, metrics) + .compile("@ShowIf(false)

hello

"); + + assert null != widget : " null "; + + final StringBuilder builder = new StringBuilder(); + + final Respond mockRespond = RespondersForTesting.newRespond(); + widget.render(new Object(), mockRespond); + + final String value = mockRespond.toString(); + assert "".equals(value) : "Did not write expected output, instead: " + value; + } + + + @Test + public final void readTextWidgetValues() { + final Evaluator evaluator = new MvelEvaluator(); + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + } + }); + + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + + Renderable widget = + new XmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, pageBook, metrics) + .compile("
hello ${name}
"); + + assert null != widget : " null "; + + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String value = mockRespond.toString(); + assert "
hello Dhanji
" + .replaceAll("'", "\"") + .equals(value) : "Did not write expected output, instead: " + value; + } + + public static class TestBackingType { + private String name; + private String clazz; + private Integer id; + + public TestBackingType(String name, String clazz, Integer id) { + this.name = name; + this.clazz = clazz; + this.id = id; + } + + public String getName() { + return name; + } + + public String getClazz() { + return clazz; + } + + public Integer getId() { + return id; + } + } + + + @Test + public final void readAndRenderRequireWidget() { + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + bind(new TypeLiteral>>() { + }) + .annotatedWith(Bricks.class) + .toInstance(methods); + } + }); + + + final PageBook pageBook = injector.getInstance(PageBook.class); + + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + + Renderable widget = + new XmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, pageBook, metrics) + .compile(" " + + " @Require " + + " @Require " + + "" + + "
hello ${name}
"); + + assert null != widget : " null "; + + final Respond respond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), respond); + + final String value = respond.toString(); + String expected = " " + + " " + + "" + + "
hello Dhanji
"; + expected = expected.replaceAll("'", "\""); + + assert expected + + + .equals(value) : "Did not write expected output, instead: " + value; + } + + + @Test + public final void readXmlWidget() { + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + Renderable widget = + new XmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, pageBook, metrics) + .compile("
hello
"); + + assert null != widget : " null "; + + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String s = mockRespond.toString(); + assert "
hello
" + .equals(s) : "Did not write expected output, instead: " + s; + } + + + @Test + public final void readXmlWidgetWithChildren() { + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + + Renderable widget = + new XmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, pageBook, metrics) + .compile("
hello @ShowIf(false)hideme
"); + + assert null != widget : " null "; + + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String s = mockRespond.toString(); + assert "
hello
" + .equals(s) : "Did not write expected output, instead: " + s; + } + + @EmbedAs(MyEmbeddedPage.MY_FAVE_ANNOTATION) + public static class MyEmbeddedPage { + protected static final String MY_FAVE_ANNOTATION = "MyFave"; + private boolean should = true; + + public boolean isShould() { + return should; + } + + public void setShould(boolean should) { + this.should = should; + } + } + + @Test + public final void readEmbedWidgetAndStoreAsPage() { + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + bind(new TypeLiteral>>() { + }) + .annotatedWith(Bricks.class) + .toInstance(methods); + } + }); + final PageBook book = injector //hacky, where are you super-packages! + .getInstance(PageBook.class); + + book.at("/somewhere", MyEmbeddedPage.class).apply(Chains.terminal()); + + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + registry.addEmbed("myfave"); + + Renderable widget = + new XmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, book, metrics) + .compile("
hello @MyFave(should=false)hideme
"); + + assert null != widget : " null "; + + //tell pagebook to track this as an embedded widget + book.embedAs(MyEmbeddedPage.class, MyEmbeddedPage.MY_FAVE_ANNOTATION) + .apply(Chains.terminal()); + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String s = mockRespond.toString(); + assert "
hello
" + .equals(s) : "Did not write expected output, instead: " + s; + } + + + @Test + public final void readEmbedWidgetOnly() { + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); + bind(new TypeLiteral>>() { + }) + .annotatedWith(Bricks.class) + .toInstance(methods); + } + }); + final PageBook book = injector //hacky, where are you super-packages! + .getInstance(PageBook.class); + + + final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); + registry.addEmbed("myfave"); + + Renderable widget = + new XmlTemplateCompiler(Object.class, new MvelEvaluatorCompiler(TestBackingType.class), registry, pageBook, metrics) + .compile("
hello @MyFave(should=false)hideme
"); + + assert null != widget : " null "; + + //tell pagebook to track this as an embedded widget + book.embedAs(MyEmbeddedPage.class, MyEmbeddedPage.MY_FAVE_ANNOTATION) + .apply(Chains.terminal()); + + final Respond mockRespond = RespondersForTesting.newRespond(); + + widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); + + final String s = mockRespond.toString(); + assert "
hello
" + .equals(s) : "Did not write expected output, instead: " + s; + } + + + //TODO Fix this test! +// @Test +// public final void readEmbedWidgetWithArgs() throws ExpressionCompileException { +// +// final Evaluator evaluator = new MvelEvaluator(); +// final Injector injector = Guice.createInjector(new AbstractModule() { +// protected void configure() { +// bind(HttpServletRequest.class).toProvider(mockRequestProviderForContext()); +// } +// }); +// final PageBook book = injector.getInstance(PageBook.class); //hacky, where are you super-packages! +// +// final WidgetRegistry registry = injector.getInstance(WidgetRegistry.class); +// +// final MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(TestBackingType.class); +// Renderable widget = +// new XmlTemplateCompiler(Object.class, compiler, registry, book, metrics) +// .compile("
hello @MyFave(should=true) @With(\"me\")

showme

"); +// +// assert null != widget : " null "; +// +// +// XmlWidget bodyWrapper = new XmlWidget(Chains.proceeding().addWidget(new IncludeWidget(new TerminalWidgetChain(), "'me'", evaluator)), +// "body", compiler, Collections.emptyMap()); +// +// bodyWrapper.setRequestProvider(mockRequestProviderForContext()); +// +// //should include the @With("me") annotated widget from the template above (discarding the

tag). +// book.embedAs(MyEmbeddedPage.class).apply(bodyWrapper); +// +// final Respond mockRespond = new StringBuilderRespond(); +// +// widget.render(new TestBackingType("Dhanji", "content", 12), mockRespond); +// +// final String s = mockRespond.toString(); +// assert "

hello showme
" +// .equals(s) : "Did not write expected output, instead: " + s; +// } + +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/headless/HeadlessReplyTest.java b/sitebricks/src/test/java/com/google/sitebricks/headless/HeadlessReplyTest.java new file mode 100644 index 00000000..0a394964 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/headless/HeadlessReplyTest.java @@ -0,0 +1,230 @@ +package com.google.sitebricks.headless; + +import static org.easymock.EasyMock.createNiceMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collections; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.sitebricks.client.transport.Json; +import com.google.sitebricks.client.transport.Text; +import com.google.sitebricks.client.transport.Xml; +import com.google.sitebricks.conversion.Converter; +import com.google.sitebricks.conversion.ConverterRegistry; +import com.google.sitebricks.conversion.StandardTypeConverter; + +/** + * A unit test for the reply builder/response populate pipeline. + */ +public class HeadlessReplyTest { + private static final String HELLO_THERE = "Hello there!"; + private static final String INK_SPOTS = "Ink Spots"; + private static final String MAYBE = "Maybe"; + private static final int SONG_LENGTH_MAYBE = 3456; + private static final String X_MY_HEADER = "X-My-Header"; + private static final String X_MY_HEADER_VAL = "X-My-Haisdjfoiajsd"; + private static final String X_YOUR_HEADER = "X-Your-Header"; + private static final String X_YOUR_HEADER_VAL = "2838L"; + + private static class FakeServletOutputStream extends ServletOutputStream { + private final ByteArrayOutputStream bout = new ByteArrayOutputStream(); + @Override + public void write(int b) throws IOException { + bout.write(b); + } + + @Override + public String toString() { + return bout.toString(); + } + } + + @Test + public void textReply() throws IOException { + Injector injector = Guice.createInjector(); + HeadlessRenderer renderer = injector.getInstance(HeadlessRenderer.class); + HttpServletResponse response = createNiceMock(HttpServletResponse.class); + ServletOutputStream outputStream = fakeServletOutputStream(); + + + // The script to expect for our mock. + response.setStatus(HttpServletResponse.SC_OK); + expect(response.getOutputStream()) + .andReturn(outputStream); + + response.setContentType(injector.getInstance(Text.class).contentType()); + + replay(response); + + renderer.render(response, Reply.with(HELLO_THERE)); + + verify(response); + + assert HELLO_THERE.equals(outputStream.toString()); + } + + @Test + public void jsonReply() throws IOException { + Injector injector = Guice.createInjector(new AbstractModule() { + @SuppressWarnings("rawtypes") + @Override + protected void configure() { + bind(ConverterRegistry.class).toInstance(new StandardTypeConverter(Collections.emptySet())); + } + }); + HeadlessRenderer renderer = injector.getInstance(HeadlessRenderer.class); + HttpServletResponse response = createNiceMock(HttpServletResponse.class); + ServletOutputStream outputStream = fakeServletOutputStream(); + + + // The script to expect for our mock. + response.setStatus(HttpServletResponse.SC_OK); + expect(response.getOutputStream()) + .andReturn(outputStream); + + response.setContentType(injector.getInstance(Json.class).contentType()); + + replay(response); + + Song maybeByTheInkspots = new Song(MAYBE, INK_SPOTS, SONG_LENGTH_MAYBE); + renderer.render(response, Reply.with(maybeByTheInkspots).as(Json.class)); + + verify(response); + + String output = outputStream.toString(); + assert output.contains(MAYBE); + assert output.contains(INK_SPOTS); + assert output.contains("" + SONG_LENGTH_MAYBE); + + // Now the real test, unmarshall it back into Java. + Song song = injector.getInstance(Json.class) + .in(new ByteArrayInputStream(output.getBytes()), Song.class); + + assert maybeByTheInkspots.hashCode() == song.hashCode(); + assert maybeByTheInkspots.equals(song); + } + + @Test + public void xmlReplyWithHeaders() throws IOException { + Injector injector = Guice.createInjector(); + HeadlessRenderer renderer = injector.getInstance(HeadlessRenderer.class); + HttpServletResponse response = createNiceMock(HttpServletResponse.class); + ServletOutputStream outputStream = fakeServletOutputStream(); + + + ImmutableMap headerMap = ImmutableMap.of( + X_MY_HEADER, X_MY_HEADER_VAL, + X_YOUR_HEADER, X_YOUR_HEADER_VAL + ); + + // The script to expect for our mock. + response.setStatus(HttpServletResponse.SC_OK); + expect(response.getOutputStream()) + .andReturn(outputStream); + + response.setContentType(injector.getInstance(Xml.class).contentType()); + response.setHeader(X_MY_HEADER, X_MY_HEADER_VAL); + response.setHeader(X_YOUR_HEADER, X_YOUR_HEADER_VAL); + + replay(response); + + Song maybeByTheInkspots = new Song(MAYBE, INK_SPOTS, SONG_LENGTH_MAYBE); + renderer.render(response, Reply.with(maybeByTheInkspots) + .as(Xml.class) + .headers(headerMap) + ); + + verify(response); + + String output = outputStream.toString(); + assert output.contains(MAYBE); + assert output.contains(INK_SPOTS); + assert output.contains("" + SONG_LENGTH_MAYBE); + + // Now the real test, unmarshall it back into Java. + Song song = injector.getInstance(Xml.class) + .in(new ByteArrayInputStream(output.getBytes()), Song.class); + + assert maybeByTheInkspots.hashCode() == song.hashCode(); + assert maybeByTheInkspots.equals(song); + } + + public static class Song { + private String name; + private String artist; + private int length; + + // Needed for Jackson (crappy) + public Song() { + } + + public Song(String name, String artist, int length) { + this.name = name; + this.artist = artist; + this.length = length; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public int getLength() { + return length; + } + + public void setLength(int length) { + this.length = length; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Song song = (Song) o; + + if (length != song.length) return false; + if (artist != null ? !artist.equals(song.artist) : song.artist != null) return false; + if (name != null ? !name.equals(song.name) : song.name != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (artist != null ? artist.hashCode() : 0); + result = 31 * result + length; + return result; + } + } + + private static ServletOutputStream fakeServletOutputStream() { + return new FakeServletOutputStream(); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/headless/ReplyEdslTest.java b/sitebricks/src/test/java/com/google/sitebricks/headless/ReplyEdslTest.java new file mode 100644 index 00000000..dcf3d4cf --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/headless/ReplyEdslTest.java @@ -0,0 +1,59 @@ +package com.google.sitebricks.headless; + +import com.google.sitebricks.client.transport.Json; +import org.testng.annotations.Test; + +/** + * A test for the reply builder api. + */ +public class ReplyEdslTest { + + @Test + public final void entities() { + + // Our default transport will convert this to a plain text string. + Reply.with(new Object()); + + // Serialized explicitly with a transport. + Reply.with(new Object()).as(Json.class); + + // Simple plain text response (the default). + Reply.with("hello there!"); + + + + } + + @Test + public final void replies() { + + //200s + Reply.saying() + .noContent(); // 204 + + + // redirects + Reply.saying() + .seeOther("/other"); // 303 + + Reply.saying() + .redirect("http://other.com/stuff"); // 302 + + + // 400s + Reply.saying() + .notFound(); // 404 + + Reply.saying() + .unauthorized(); // 401 + + Reply.saying() + .forbidden(); // 403 + + + + // others + Reply.saying() + .error(); // 500 + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/http/negotiate/ExactMatchNegotiatorTest.java b/sitebricks/src/test/java/com/google/sitebricks/http/negotiate/ExactMatchNegotiatorTest.java new file mode 100644 index 00000000..aab0a8ec --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/http/negotiate/ExactMatchNegotiatorTest.java @@ -0,0 +1,79 @@ +package com.google.sitebricks.http.negotiate; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterators; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.util.Enumeration; +import java.util.Map; + +import static org.easymock.EasyMock.createMock; + +/** + * Unit test for ExactMatchNegotiator + */ +public class ExactMatchNegotiatorTest { + private static final String HEADERS_AND_NEGOTIATIONS = "HEADERS_NEGS"; + + @DataProvider(name = HEADERS_AND_NEGOTIATIONS) + public Object[][] headersAndNegotiations() { + return new Object[][] { + { ImmutableMap.of(), Multimaps.forMap(ImmutableMap.of()), true }, + + // negotation, but no headers matching + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of()), false }, + + // headers but no negs + { ImmutableMap.of(), Multimaps.forMap(ImmutableMap.of("Accept", "image/png")), true }, + + // disjoint set of headers and negs + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Content-Accept", "image/png")), false }, + + // Non matching set of the same header + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Accept", "image/png")), false }, + + // Matching set of the same header + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing")), true }, + + // Matching set of the same header, but case-mismatch + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Accept", "THING")), false }, + + // Multiple header values, one matches + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Accept", "nonthing, thing")), true }, + + // Multiple header values, one matches, different order + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo")), true }, + + // Multiple header values, some match, some don't + { ImmutableMap.of("Accept", "thing", "Content-Accept", "nothing"), + Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo")), + false }, + + // Multiple header values, some match, some don't, but all passes + { ImmutableMap.of("Accept", "thing", "Content-Accept", "nothing"), + Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo", "Content-Accept", "aisdjf, nothing, aiosjdf")), + true }, + + }; + } + + + @Test(dataProvider = HEADERS_AND_NEGOTIATIONS) + public final void variousHeadersAndNegotiations(Map negotiations, + final Multimap headers, + boolean shouldPass) { + HttpServletRequest request = new HttpServletRequestWrapper(createMock(HttpServletRequest.class)) { + @Override + public Enumeration getHeaders(String name) { + return Iterators.asEnumeration(headers.get(name).iterator()); + } + }; + + assert shouldPass == new ExactMatchNegotiator().shouldCall(negotiations, request); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/http/negotiate/RegexNegotiatorTest.java b/sitebricks/src/test/java/com/google/sitebricks/http/negotiate/RegexNegotiatorTest.java new file mode 100755 index 00000000..15217530 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/http/negotiate/RegexNegotiatorTest.java @@ -0,0 +1,104 @@ +package com.google.sitebricks.http.negotiate; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterators; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.util.Enumeration; +import java.util.Map; + +import static org.easymock.EasyMock.createMock; + +public class RegexNegotiatorTest { + private static final String HEADERS_AND_NEGOTIATIONS = "HEADERS_NEGS"; + + @DataProvider(name = HEADERS_AND_NEGOTIATIONS) + public Object[][] headersAndNegotiations() { + return new Object[][] { + { ImmutableMap.of(), Multimaps.forMap(ImmutableMap.of()), true }, + + // negotation, but no headers matching + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of()), false }, + { ImmutableMap.of("Accept", "thing, other-thing"), Multimaps.forMap(ImmutableMap.of()), false }, + + // headers but no negs + { ImmutableMap.of(), Multimaps.forMap(ImmutableMap.of("Accept", "image/png")), true }, + + // disjoint set of headers and negs + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Content-Accept", "image/png")), false }, + { ImmutableMap.of("Accept", ".*thing.*"), Multimaps.forMap(ImmutableMap.of("Content-Accept", "image/png")), false }, + + // Non matching set of the same header + { ImmutableMap.of("Accept", ".*thing.*"), Multimaps.forMap(ImmutableMap.of("Accept", "image/png")), false }, + + // Matching set of the same header + { ImmutableMap.of("Accept", ".*thing.*"), Multimaps.forMap(ImmutableMap.of("Accept", "thing")), true }, + + // Matching set of the same header, but different cases + { ImmutableMap.of("Accept", ".*thing.*"), Multimaps.forMap(ImmutableMap.of("Accept", "THING")), false }, + + // Multiple header values, one matches + { ImmutableMap.of("Accept", ".*thing.*"), Multimaps.forMap(ImmutableMap.of + ("Accept", "nonthing, thing")), true }, + { ImmutableMap.of("Referer", ".*(google|yahoo|bing)\\.com.*"), Multimaps.forMap(ImmutableMap.of + ("Accept", "text/*, nonthing", "Referer", "http://google.com/")), true }, + { ImmutableMap.of("Accept", ".*text/.*"), Multimaps.forMap(ImmutableMap.of + ("Accept", "nonthing, text/plain", "Referer", "http://google.com/")), true }, + + // Multiple header values, one matches, different order + { ImmutableMap.of("Accept", ".*thing.*"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo")), true }, + + // Multiple header values, some match, some don't + { ImmutableMap.of("Accept", "thing", "Content-Accept", "nothing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo")), false }, + + // Multiple header values, some match, some don't, but all passes + { ImmutableMap.of("Accept", ".*thing.*", "Content-Accept", ".*nothing.*"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo", "Content-Accept", "aisdjf, nothing, aiosjdf")), true }, + + + + + // from the wild + { ImmutableMap.of("Accept", ".*text/(\\*|html).*"), Multimaps.forMap(ImmutableMap.of + ("Accept", "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5")), + true }, + { ImmutableMap.of("Accept", ".*/xml.*"), Multimaps.forMap(ImmutableMap.of + ("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")), + true }, + { ImmutableMap.of("Accept", ".*(application|text)/.*xml.*"), Multimaps.forMap(ImmutableMap.of + ("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")), + true }, + { ImmutableMap.of("Accept", ".*(application|text)/.*xml.*"), Multimaps.forMap(ImmutableMap.of + ("Accept", "application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5")), + true }, + { ImmutableMap.of("Accept", ".*application/.*(silverlight|flash|shockwave).*"), Multimaps.forMap(ImmutableMap.of + ("Accept", "image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/msword, " + + "application/vnd.ms-xpsdocument, application/xaml+xml, application/x-ms-xbap, application/x-shockwave-flash, " + + "application/x-silverlight-2-b2, application/x-silverlight, application/vnd.ms-excel, application/vnd.ms-powerpoint, */*")), + true }, + + + + + }; + } + + + @Test(dataProvider = HEADERS_AND_NEGOTIATIONS) + public final void variousHeadersAndNegotiations(Map negotiations, + final Multimap headers, + boolean shouldPass) { + HttpServletRequest request = new HttpServletRequestWrapper(createMock(HttpServletRequest.class)) { + @Override + public Enumeration getHeaders(String name) { + return Iterators.asEnumeration(headers.get(name).iterator()); + } + }; + + assert shouldPass == new RegexNegotiator().shouldCall(negotiations, request); + } +} \ No newline at end of file diff --git a/sitebricks/src/test/java/com/google/sitebricks/http/negotiate/WildcardNegotiatorTest.java b/sitebricks/src/test/java/com/google/sitebricks/http/negotiate/WildcardNegotiatorTest.java new file mode 100755 index 00000000..1906076e --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/http/negotiate/WildcardNegotiatorTest.java @@ -0,0 +1,124 @@ +package com.google.sitebricks.http.negotiate; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterators; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.util.Enumeration; +import java.util.Map; + +import static org.easymock.EasyMock.createMock; + +public class WildcardNegotiatorTest { + private static final String HEADERS_AND_NEGOTIATIONS = "HEADERS_NEGS"; + + @DataProvider(name = HEADERS_AND_NEGOTIATIONS) + public Object[][] headersAndNegotiations() { + return new Object[][] { + { ImmutableMap.of(), Multimaps.forMap(ImmutableMap.of()), true }, + + // negotation, but no headers matching + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of()), false }, + { ImmutableMap.of("Accept", "thing, other-thing"), Multimaps.forMap(ImmutableMap.of()), false }, + + // headers but no negs + { ImmutableMap.of(), Multimaps.forMap(ImmutableMap.of("Accept", "image/png")), true }, + + // disjoint set of headers and negs + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Content-Accept", "image/png")), false }, + { ImmutableMap.of("Accept", "thing, text/something, image/gif"), Multimaps.forMap(ImmutableMap.of("Content-Accept", "image/png")), false }, + + // Non matching set of the same header + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Accept", "image/png")), false }, + { ImmutableMap.of("Accept", "thing, text/something, image/gif"), Multimaps.forMap(ImmutableMap.of("Accept", "image/png")), false }, + + // Matching set of the same header + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing")), true }, + { ImmutableMap.of("Accept", "thing, other-thing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing")), true }, + { ImmutableMap.of("Accept", "thing, image/*"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, image/tiff")), true }, + { ImmutableMap.of("Accept", "thing, other-thing, image/tiff"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, image/*")), true }, + + // Matching set of the same header, but different cases + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Accept", "THING")), false }, + { ImmutableMap.of("Accept", "thing, other-thing"), Multimaps.forMap(ImmutableMap.of("Accept", "THING, OTHER-THING")), false }, + + // Matching set of the same header, match different cases in mediatypes (according to spec) + { ImmutableMap.of("Accept", "thing, text/WhAtEveR, FOo/bar"), Multimaps.forMap(ImmutableMap.of("Accept", "THING, OTHER-THING, text/whatever, foo/bar")), true }, + { ImmutableMap.of("Accept", "this/THAT, FOO/bar, whoa/*"), Multimaps.forMap(ImmutableMap.of("Accept", "NOTHING, whoa/PLAIN, THIS/that, foo/BAR")), true }, + + // Multiple header values, one matches + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Accept", "nonthing, thing")), true }, + { ImmutableMap.of("Accept", "thing, other-thing"), Multimaps.forMap(ImmutableMap.of("Accept", "nonthing, thing")), true }, + + // Multiple header values, non media-type one match + { ImmutableMap.of("Accept", "thing, text/*"), Multimaps.forMap(ImmutableMap.of("Accept", "nonthing, thing")), true }, + { ImmutableMap.of("Accept", "thing, nonthing"), Multimaps.forMap(ImmutableMap.of("Accept", "text/*, nonthing")), true }, + + // Multiple header values, one matches, different order + { ImmutableMap.of("Accept", "thing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo")), true }, + { ImmutableMap.of("Accept", "another-thing, thing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo")), true }, + + // Multiple header values, some match, some don't + { ImmutableMap.of("Accept", "thing", "Content-Accept", "nothing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo")), false }, + { ImmutableMap.of("Accept", "thing, another-thing/coming", "Content-Accept", "nothing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo")), false }, + { ImmutableMap.of("Accept", "thing", "Content-Accept", "nothing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo")), false }, + { ImmutableMap.of("Accept", "thing, text/*, another-thing/coming", "Content-Accept", "nothing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, image/*, hno, asdo")), false }, + + // Multiple header values, some match, some don't, but all passes + { ImmutableMap.of("Accept", "thing", "Content-Accept", "nothing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo", "Content-Accept", "aisdjf, nothing, aiosjdf")), true }, + { ImmutableMap.of("Accept", "thing, another-thing, something", "Content-Accept", "nothing, kaupthing, clothing"), Multimaps.forMap(ImmutableMap.of("Accept", "thing, hno, asdo", "Content-Accept", "aisdjf, nothing, aiosjdf")), true }, + + // Multiple header values, one matches wildcard neg subtype + { ImmutableMap.of("Accept", "thing, text/foo, text/*"), Multimaps.forMap(ImmutableMap.of("Accept", "nonthing, text/plain")), true }, + { ImmutableMap.of("Accept", "image/*, text/plain, text/*"), Multimaps.forMap(ImmutableMap.of("Accept", "nonthing, text/html, image/tiff")), true }, + + // Multiple header values, one with wildcard subtype, matches + { ImmutableMap.of("Accept", "thing, text/foo, text/plain"), Multimaps.forMap(ImmutableMap.of("Accept", "nonthing, text/*")), true }, + { ImmutableMap.of("Accept", "image/png, text/plain, text/ual"), Multimaps.forMap(ImmutableMap.of("Accept", "nonthing, text/html, image/*")), true }, + + + // from the wild + { ImmutableMap.of("Accept", "text/html"), Multimaps.forMap(ImmutableMap.of + ("Accept", "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5")), + true }, + { ImmutableMap.of("Accept", "application/xhtml+xml, application/xml"), Multimaps.forMap(ImmutableMap.of + ("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")), + true }, + { ImmutableMap.of("Accept", "application/xhtml+xml, application/xml"), Multimaps.forMap(ImmutableMap.of + ("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")), + true }, + { ImmutableMap.of("Accept", "application/xhtml+xml, application/xml"), Multimaps.forMap(ImmutableMap.of + ("Accept", "application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5")), + true }, + { ImmutableMap.of("Accept", "application/x-silverlight, application/ho-ho-ho"), Multimaps.forMap(ImmutableMap.of + ("Accept", "image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/msword, " + + "application/vnd.ms-xpsdocument, application/xaml+xml, application/x-ms-xbap, application/x-shockwave-flash, " + + "application/x-silverlight-2-b2, application/x-silverlight, application/vnd.ms-excel, application/vnd.ms-powerpoint, */*")), + true }, + + + + + }; + } + + + @Test(dataProvider = HEADERS_AND_NEGOTIATIONS) + public final void variousHeadersAndNegotiations(Map negotiations, + final Multimap headers, + boolean shouldPass) { + HttpServletRequest request = new HttpServletRequestWrapper(createMock(HttpServletRequest.class)) { + @Override + public Enumeration getHeaders(String name) { + return Iterators.asEnumeration(headers.get(name).iterator()); + } + }; + + assert shouldPass == new WildcardNegotiator().shouldCall(negotiations, request); + } +} \ No newline at end of file diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/DynTypedMvelEvaluatorCompiler.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/DynTypedMvelEvaluatorCompiler.java new file mode 100644 index 00000000..93dd3c15 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/DynTypedMvelEvaluatorCompiler.java @@ -0,0 +1,71 @@ +package com.google.sitebricks.rendering; + +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.Parsing; +import com.google.sitebricks.compiler.Token; + +import org.jetbrains.annotations.Nullable; +import org.mvel2.MVEL; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + + +/** + * Temporary class to enable dynamic typing for collection projections (since we don't have + * a good mechanism in MVEL to reflect on parametric types yet) + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class DynTypedMvelEvaluatorCompiler implements EvaluatorCompiler { + + public DynTypedMvelEvaluatorCompiler(Map> map) { + } + + public Evaluator compile(final String expression) throws ExpressionCompileException { + return new Evaluator() { + private final ConcurrentMap map = new ConcurrentHashMap(); + + @Nullable + public Object evaluate(String ___expr, Object bean) { + Serializable serializable = map.get(expression); + + if (null == serializable) { + serializable = MVEL.compileExpression(expression); + map.put(expression, serializable); + } + + return MVEL.executeExpression(serializable, bean); + } + + public void write(String expr, Object bean, Object value) { + } + + public Object read(String property, Object contextObject) { + return MVEL.getProperty(property, contextObject); + } + }; + } + + public Class resolveCollectionTypeParameter(String expression) throws ExpressionCompileException { + return Object.class; + } + + public List tokenizeAndCompile(String template) throws ExpressionCompileException { + return Parsing.tokenize(template, this); + } + + public Class resolveEgressType(String expression) throws ExpressionCompileException { + return Collection.class; + } + + public boolean isWritable(String property) throws ExpressionCompileException { + return true; + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/EvaluatorCompilerTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/EvaluatorCompilerTest.java new file mode 100644 index 00000000..71b37c21 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/EvaluatorCompilerTest.java @@ -0,0 +1,260 @@ +package com.google.sitebricks.rendering; + +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.MvelEvaluatorCompiler; +import com.google.sitebricks.conversion.generics.Generics; + +import org.testng.annotations.Test; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class EvaluatorCompilerTest { + private static final String A_NAME = "Dhanji"; + + @Test + public final void compileEvaluatorFromExpression() throws ExpressionCompileException { + Evaluator compiled = new MvelEvaluatorCompiler(AType.class) + .compile("name"); + + //reading expression + Object value = compiled.evaluate(null, new AType(A_NAME)); + + assert A_NAME == value; + } + + @Test + public final void compileExpressionInvokingArbitraryMethod() throws ExpressionCompileException { + Evaluator compiled = new MvelEvaluatorCompiler(AType.class) + .compile("b.sigmatron('Hi')"); + + //reading expression + assert "Hi".equals(compiled.evaluate(null, new AType(A_NAME))); + + } + + @Test + public final void compileExpressionInvokingArbitraryMethodAndTestReturn() throws ExpressionCompileException { + Evaluator compiled = new MvelEvaluatorCompiler(AType.class) + .compile("b.sigmatron(null)"); + + //reading expression + assert null == (compiled.evaluate(null, new AType(A_NAME))); + + } + + @Test(expectedExceptions = ExpressionCompileException.class) + public final void failCompileExpressionInvokingArbitraryMethodThruInterface() throws ExpressionCompileException { + Evaluator compiled = new MvelEvaluatorCompiler(AType.class) + .compile("b.bkind.sigmatron(null)"); + + //reading expression + assert null == (compiled.evaluate(null, new AType(A_NAME))); + + } + + @Test + public final void compileExpressionInvokingArbitraryMethodThruInterface() throws ExpressionCompileException { + Evaluator compiled = new MvelEvaluatorCompiler(AType.class) + .compile("bkind.getDubdub()"); + + //reading expression + final AType anA = new AType(A_NAME); + assert anA.getB().getDubdub().equals(compiled.evaluate(null, anA)); + + } + + @Test + public final void compileExpressionInvokingArbitraryMethodThruInterfaceAndRegular() throws ExpressionCompileException { + Evaluator compiled = new MvelEvaluatorCompiler(AType.class) + .compile("bkind.getDubdub() == b.dubdub"); + + //reading expression + //noinspection ConstantConditions + assert (Boolean)compiled.evaluate(null, new AType(A_NAME)); + + } + + @Test(expectedExceptions = ExpressionCompileException.class) + public final void failCompileExpressionInvokingArbitraryMethodWithWrongArgs() throws ExpressionCompileException { + Evaluator compiled = new MvelEvaluatorCompiler(AType.class) + .compile("b.sigmatron()"); + + } + + @Test(expectedExceptions = ExpressionCompileException.class) + public final void failCompileDueToNameMismatch() throws ExpressionCompileException { + new MvelEvaluatorCompiler(AType.class) + .compile("anythingaling"); + + + } + + @Test(expectedExceptions = ExpressionCompileException.class) + public final void failCompileDueToNameMismatchInDeeperObjectGraph() throws ExpressionCompileException { + new MvelEvaluatorCompiler(AType.class) + .compile("name.anythingaling"); + + } + + @Test(expectedExceptions = ExpressionCompileException.class) + public final void failCompileDueToNameMismatchInDeeperObjectGraph2() throws ExpressionCompileException { + new MvelEvaluatorCompiler(AType.class) + .compile("name.b.anythingaling"); + + } + + // DISABLED. GIVE TEST TO MIKE BROCK +// @Test(expectedExceptions = ExpressionCompileException.class) + public final void failCompileDueToMethodMismatchInDeeperObjectGraph() throws ExpressionCompileException { + new MvelEvaluatorCompiler(AType.class) + .compile("b.a.b.name.substring(1)"); + + } + + @Test + public final void compileMethodMatchInDeeperObjectGraph() throws ExpressionCompileException { + assert A_NAME.substring(1).equals(new MvelEvaluatorCompiler(AType.class) + .compile("b.aString.substring(1)") + .evaluate(null, new AType(A_NAME))); + + + } + + @Test + public final void compileExpressionIntegerTypeMatchInDeeperObjectGraph() throws ExpressionCompileException { + new MvelEvaluatorCompiler(AType.class) + .compile("b.a.b.a.b.name / 44"); + + } + + @Test + public final void compileExpressionNumericTypeMatchInDeeperObjectGraph() throws ExpressionCompileException { + new MvelEvaluatorCompiler(AType.class) + .compile("b.a.b.a.b.dubdub / new Double(44.0)"); + + } + +// @Test(expectedExceptions = ExpressionCompileException.class) DISABLED TEMPORARILY + public final void failCompileExpressionNumericTypeMismatchInDeeperObjectGraph() throws ExpressionCompileException { + // TODO(dhanji): MVEL bug + new MvelEvaluatorCompiler(AType.class) + .compile("b.a.b.a.b.name / new Double(44.0)"); + + } + + @Test(expectedExceptions = ExpressionCompileException.class) + public final void failCompileDueToPathMismatchInDeeperObjectGraph() throws ExpressionCompileException { + new MvelEvaluatorCompiler(AType.class) + .compile("name.b.a.b.name + 2"); + + } + +// @Test(expectedExceptions = ExpressionCompileException.class) DISABLED TEMPORARILY + public final void failCompileDueToTypeMismatchInDeeperObjectGraph() throws ExpressionCompileException { + // TODO(dhanji): fix in mvel!!!!! + new MvelEvaluatorCompiler(AType.class) + .compile("b.a.name - 2"); + } + +// @Test DISABLED TEMPORARILY + public final void compileTypeMatchInDeeperObjectGraph() throws ExpressionCompileException { + //should not throw exception + + // TODO(dhanji): Fix this in mvel, this is a problem with the egress type detection in IntSub + final MvelEvaluatorCompiler evaluatorCompiler = new MvelEvaluatorCompiler(AType.class); +// evaluatorCompiler.compile("b.name - 2"); + + assert Integer.class.isAssignableFrom(Generics.erase(evaluatorCompiler.resolveEgressType("b.name - 2"))); + } + +// @Test(expectedExceptions = ExpressionCompileException.class) DISABLED TEMPORARILY + public final void failCompileDueToTypeMismatch() throws ExpressionCompileException { + // TODO(dhanji) fix in mvel + new MvelEvaluatorCompiler(AType.class) + .compile("name - 2"); + } + + @Test + public final void determineEgressTypeParameter() throws ExpressionCompileException { + final Type egressType = new MvelEvaluatorCompiler(AType.class) + .resolveEgressType("bs"); + + assert BType.class.equals(Generics.erase(Generics.getTypeParameter(egressType, Collection.class.getTypeParameters()[0]))); + } + + @Test + public final void determineEgressTypeParameterInExpressionChain() throws ExpressionCompileException { + final Type egressType = new MvelEvaluatorCompiler(BType.class) + .resolveEgressType("a.bs"); + assert BType.class.equals(Generics.erase(Generics.getTypeParameter(egressType, Collection.class.getTypeParameters()[0]))); + } + + public static class AType { + private String name; + private BType b = new BType(45); + private BKind bkind = new BType(400); + + public AType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public BType getB() { + return b; + } + + public BKind getBkind() { + return bkind; + } + + public List getBs() { + return Arrays.asList(new BType(1)); + } + } + + public static class BType implements BKind { + private Integer name; + private Double dubdub = 100.0; + private String aString = A_NAME; + + public AType getA() { + return a; + } + + private AType a; + + public BType(Integer name) { + this.name = name; + } + + public Integer getName() { + return name; + } + + public Double getDubdub() { + return dubdub; + } + + public String sigmatron(String s) { + return s; + } + + public String getAString() { + return aString; + } + } + + public static interface BKind { + Double getDubdub(); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/MvelGenericsConfidenceTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/MvelGenericsConfidenceTest.java new file mode 100644 index 00000000..e378ed3a --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/MvelGenericsConfidenceTest.java @@ -0,0 +1,67 @@ +package com.google.sitebricks.rendering; + +import org.mvel2.MVEL; +import org.mvel2.ParserContext; +import org.mvel2.compiler.CompiledExpression; +import org.mvel2.compiler.ExpressionCompiler; +import org.testng.annotations.Test; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class MvelGenericsConfidenceTest { + private static final List STRINGS = Arrays.asList("hi", "there"); + + @Test + public final void determineEgressParametricType() { + final ParserContext parserContext = new ParserContext(); + parserContext.setStrongTyping(true); + parserContext.addInput("strings", List.class, new Class[] { String.class }); + + final CompiledExpression expr = new ExpressionCompiler("strings", parserContext) + .compile(); + + assert STRINGS.equals(MVEL.executeExpression(expr, new A())) : "faulty expression eval"; + + final Type[] typeParameters = expr.getParserContext().getLastTypeParameters(); + + assert null != typeParameters : "no generic egress type"; + assert String.class.equals(typeParameters[0]) : "wrong generic egress type"; + } + + @Test + public final void determineEgressParametricTypeInExprChain() { + final ParserContext parserContext = new ParserContext(); + parserContext.setStrongTyping(true); + parserContext.addInput("strings", A.class); + + final CompiledExpression expr = new ExpressionCompiler("strings.strings", parserContext) + .compile(); + + assert STRINGS.equals(MVEL.executeExpression(expr, new B())) : "faulty expression eval"; + + final Type[] typeParameters = expr.getParserContext().getLastTypeParameters(); + + assert null != typeParameters : "no generic egress type"; + assert String.class.equals(typeParameters[0]) : "wrong generic egress type"; + + } + + public static class A { + + public List getStrings() { + return STRINGS; + } + } + + public static class B { + + public A getStrings() { + return new A(); + } + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/ParsingTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/ParsingTest.java new file mode 100644 index 00000000..0b594795 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/ParsingTest.java @@ -0,0 +1,76 @@ +package com.google.sitebricks.rendering; + +import com.google.sitebricks.compiler.Parsing; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class ParsingTest { + private static final String XML_AND_FLAT_TEMPLATES = "XMLandFlats"; + + @DataProvider(name = XML_AND_FLAT_TEMPLATES) + public Object[][] get() { + return new Object[][]{ + {" @Meta() Hello world!", false}, + {" @Meta() Hello world!", true}, + {" @Meta() world!", false}, + {" boundTo = Arrays.asList(s1, s2, s3); + + @SuppressWarnings("unchecked") + final Provider cacheProvider = createMock(Provider.class); + final FlashCache cache = createMock(FlashCache.class); + + expect(cacheProvider.get()) + .andReturn(cache); + + cache.put("strings", boundTo); + + replay(cacheProvider, cache); + + + final ChooseWidget widget = new ChooseWidget(new ProceedingWidgetChain(), "from=strings, bind=choice", new MvelEvaluator()); + widget.setCache(cacheProvider); + + widget.render(new HashMap() {{ + put("strings", boundTo); + }}, respond); + + + //assert the validity of the text tag: + String tag = respond.toString(); + +// System.out.println(tag); + assert tag.startsWith(""); + + verify(cacheProvider, cache); + } +} \ No newline at end of file diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/control/EmbedWidgetTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/EmbedWidgetTest.java new file mode 100644 index 00000000..20c7f9c2 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/EmbedWidgetTest.java @@ -0,0 +1,467 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.MvelEvaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.RespondersForTesting; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.HtmlTemplateCompilerTest; +import com.google.sitebricks.compiler.MvelEvaluatorCompiler; +import com.google.sitebricks.routing.PageBook; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.easymock.EasyMock.*; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class EmbedWidgetTest { + private static final String PAGES_FOR_EMBEDDING = "pagesForEmbedding"; + private static final String PAGES_FOR_EMBEDDING_BROKEN = "pagesForEmbeddingBroken"; + private static final String PAGES_FOR_EMBEDDING_BROKEN_EXCEPTION = "pagesForEmbeddingBrokenThrowing"; + private static final String HELLO_FROM_INCLUDE = "helloFromEmbed"; + + @DataProvider(name = PAGES_FOR_EMBEDDING) + public Object[][] getPages() { + return new Object[][]{ + {"MyEmbeddedPage", "aString", "message=pass"}, + {"YourPage", "anotherString", "message=pass"}, + {"YourPage2", "aaaa", "message='aaaa'"}, + {"YourPage3", "aaaa", "message=\"aaaa\""}, + {"YourPage4", "bbbb", "message=\"aaaa\", message='bb' + 'bb'"}, + {"YourPage5", "cccc", "message=\"aaaa\", message='bb' + 'bb', message = getPass()"}, + }; + } + + @DataProvider(name = EmbedWidgetTest.PAGES_FOR_EMBEDDING_BROKEN) + public Object[][] getPagesBroken() { + return new Object[][]{ + {"YourPage", "anotherString", " "}, + {"YourPage", "anotherString", ""}, + {"YourPage", "anotherString", ",,"}, + {"YourPage2", "aaaa", "message='aa'"}, + {"YourPage2", "aaaa", "message='aa',"}, + {"YourPage3", "aaaa", "message=\"aaa\""}, + {"YourPage4", "bbbbb", "message=\"aaaa\", message='bb' + 'bb'"}, + {"YourPage4", "bbbbb", "message=\"aaaa\", message='bb' + 'bb',,"}, + }; + } + + @DataProvider(name = EmbedWidgetTest.PAGES_FOR_EMBEDDING_BROKEN_EXCEPTION) + public Object[][] getPagesBrokenThrowing() { + return new Object[][]{ + {"MyEmbeddedPage", "aString", "=pass"}, + {"YourPage", "anotherString", "message="}, + {"YourPage", "anotherString", "message=pass=pass"}, + }; + } + + //the parent page object + public static class MyParentPage { + private String pass; + + MyParentPage(String pass) { + this.pass = pass; + } + + public String getPass() { + return pass; + } + } + + public static class MyEmbeddedPage { + private boolean set; + private String message; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + assert null != message; + assert !"".equals(message.trim()); + set = true; + } + + public boolean isSet() { + return set; + } + } + + @Test(dataProvider = PAGES_FOR_EMBEDDING) + public final void pageEmbeddingSupressesNormalWidgetChain(String pageName, final String passOn, final String expression) throws ExpressionCompileException { + //forName expects pageName to be in all-lower case (it's an optimization) + pageName = pageName.toLowerCase(); + + final PageBook pageBook = createMock(PageBook.class); + final PageBook.Page page = createMock(PageBook.Page.class); + final Respond respond = RespondersForTesting.newRespond(); + final Renderable widget = createMock(Renderable.class); + + expect(pageBook.forName(pageName)) + .andReturn(page); + + + //mypage does? + final MyEmbeddedPage myEmbeddedPage = new MyEmbeddedPage(); + expect(page.instantiate()) + .andReturn(myEmbeddedPage); + + expect(page.doMethod(isA(String.class), anyObject(), isA(String.class), + isA(HttpServletRequest.class))) + .andReturn(null); + expect(page.widget()) + .andReturn(widget); + + widget.render(eq(myEmbeddedPage), isA(Respond.class)); + + + replay(pageBook, page, widget); + + final MvelEvaluator evaluator = new MvelEvaluator(); + final WidgetChain widgetChain = new ProceedingWidgetChain(); + final WidgetChain targetWidgetChain = new ProceedingWidgetChain(); + + //noinspection unchecked + targetWidgetChain.addWidget(new XmlWidget(new TerminalWidgetChain(), "p", createMock(EvaluatorCompiler.class), Collections.EMPTY_MAP)); + widgetChain.addWidget(new ShowIfWidget(targetWidgetChain, "true", evaluator)); + + final EmbedWidget embedWidget = new EmbedWidget(Collections.emptyMap(), expression, evaluator, pageBook, pageName); + embedWidget.init(new EmbeddedRespondFactory(RespondersForTesting.newRespond()), HtmlTemplateCompilerTest.mockRequestProviderForContext()); + embedWidget + .render(new MyParentPage(passOn), respond); + + //assert bindings + assert myEmbeddedPage.isSet() : "variable not passed on to embedded page"; + assert passOn.equals(myEmbeddedPage.getMessage()) : "variable not set on embedded page"; + + //the render was ok + final String resp = respond.toString(); + assert "".equals(resp) : "widget not embedded correctly : " + resp; + + verify(pageBook, page, widget); + } + + + @Test(dataProvider = PAGES_FOR_EMBEDDING) + public final void pageEmbeddingChainsToEmbeddedWidget(String pageName, final String passOn, final String expression) throws ExpressionCompileException { + + //forName expects pageName to be in all-lower case (it's an optimization) + pageName = pageName.toLowerCase(); + + final PageBook pageBook = createMock(PageBook.class); + final PageBook.Page page = createMock(PageBook.Page.class); + final Respond respond = RespondersForTesting.newRespond(); + + + final MvelEvaluator evaluator = new MvelEvaluator(); + + final WidgetChain widget = new ProceedingWidgetChain(); + final WidgetChain targetWidgetChain = new ProceedingWidgetChain(); + + //noinspection unchecked + targetWidgetChain.addWidget(new XmlWidget(new TerminalWidgetChain(), "p", new MvelEvaluatorCompiler(Object.class), + new LinkedHashMap() {{ + put("class", "pretty"); + put("id", "a-p-tag"); + }})); + widget.addWidget(new ShowIfWidget(targetWidgetChain, "true", evaluator)); + + Renderable bodyWrapper = new XmlWidget(widget, "body", createMock(EvaluatorCompiler.class), Collections.emptyMap()); + + expect(pageBook.forName(pageName)) + .andReturn(page); + + + //mypage does? + final MyEmbeddedPage myEmbeddedPage = new MyEmbeddedPage(); + expect(page.instantiate()) + .andReturn(myEmbeddedPage); + + expect(page.doMethod(isA(String.class), anyObject(), isA(String.class), + isA(HttpServletRequest.class))) + .andReturn(null); + expect(page.widget()) + .andReturn(bodyWrapper); + + replay(pageBook, page); + + + final EmbedWidget embedWidget = new EmbedWidget(Collections.emptyMap(), expression, evaluator, pageBook, pageName); + embedWidget.init(new EmbeddedRespondFactory(RespondersForTesting.newRespond()), HtmlTemplateCompilerTest.mockRequestProviderForContext()); + embedWidget + .render(new MyParentPage(passOn), respond); + + + //assert bindings + assert myEmbeddedPage.isSet() : "variable not passed on to embedded page"; + assert passOn.equals(myEmbeddedPage.getMessage()) : "variable not set on embedded page"; + + //the render was ok + final String resp = respond.toString(); + assert "

".equals(resp) : "widget not embedded correctly : " + resp; + + verify(pageBook, page); + } + + + @Test(dataProvider = PAGES_FOR_EMBEDDING) + public final void pageEmbeddingChainsToEmbeddedWidgetWithArgs(String targetPageName, final String passOn, final String expression) throws ExpressionCompileException { + + //forName expects pageName to be in all-lower case (it's an optimization) + targetPageName = targetPageName.toLowerCase(); + + final PageBook pageBook = createMock(PageBook.class); + final PageBook.Page page = createMock(PageBook.Page.class); + final Respond respond = RespondersForTesting.newRespond(); + + + final MvelEvaluator evaluator = new MvelEvaluator(); + + final WidgetChain widget = new ProceedingWidgetChain(); + final WidgetChain targetWidgetChain = new ProceedingWidgetChain(); + //noinspection unchecked + targetWidgetChain.addWidget(new XmlWidget(new ProceedingWidgetChain() + .addWidget(new IncludeWidget(new TerminalWidgetChain(), "'me'", evaluator)), + + "p", new MvelEvaluatorCompiler(Object.class), new LinkedHashMap() {{ + put("class", "pretty"); + put("id", "a-p-tag"); + }})); + widget.addWidget(new ShowIfWidget(targetWidgetChain, "true", evaluator)); + + Renderable bodyWrapper = new XmlWidget(widget, "body", createMock(EvaluatorCompiler.class), Collections.emptyMap()); + + expect(pageBook.forName(targetPageName)) + .andReturn(page); + + + //mypage does? + final MyEmbeddedPage myEmbeddedPage = new MyEmbeddedPage(); + expect(page.instantiate()) + .andReturn(myEmbeddedPage); + + expect(page.doMethod(isA(String.class), anyObject(), isA(String.class), + isA(HttpServletRequest.class))) + .andReturn(null); + expect(page.widget()) + .andReturn(bodyWrapper); + + replay(pageBook, page); + + //create embedding arguments + final String includeExpr = "me"; + + Map inners = new HashMap(); + inners.put(includeExpr, new ArgumentWidget(new ProceedingWidgetChain().addWidget(new TextWidget(HELLO_FROM_INCLUDE, new MvelEvaluatorCompiler(Object.class))), + includeExpr, evaluator)); + + + final EmbedWidget embedWidget = new EmbedWidget(inners, expression, evaluator, pageBook, targetPageName); + embedWidget.init(new EmbeddedRespondFactory(RespondersForTesting.newRespond()), HtmlTemplateCompilerTest.mockRequestProviderForContext()); + embedWidget + .render(new MyParentPage(passOn), respond); + + //assert bindings + assert myEmbeddedPage.isSet() : "variable not passed on to embedded page"; + assert passOn.equals(myEmbeddedPage.getMessage()) : "variable not set on embedded page"; + + //the render was ok + final String resp = respond.toString(); + assert String.format("

%s

", HELLO_FROM_INCLUDE).equals(resp) + : "widget not embedded correctly : " + resp; + + verify(pageBook, page); + } + + + @Test(dataProvider = PAGES_FOR_EMBEDDING) + public final void pageEmbeddingChainsToEmbeddedWidgetBehavior(String pageName, final String passOn, final String expression) throws ExpressionCompileException { + + //forName expects pageName to be in all-lower case (it's an optimization) + pageName = pageName.toLowerCase(); + + final PageBook pageBook = createMock(PageBook.class); + final PageBook.Page page = createMock(PageBook.Page.class); + final Respond respond = RespondersForTesting.newRespond(); + + + final MvelEvaluator evaluator = new MvelEvaluator(); + + final ProceedingWidgetChain widget = new ProceedingWidgetChain(); + final WidgetChain targetWidgetChain = new ProceedingWidgetChain(); + //noinspection unchecked + targetWidgetChain.addWidget(new XmlWidget(new TerminalWidgetChain(), "p", new MvelEvaluatorCompiler(Object.class), new LinkedHashMap() {{ + put("class", "pretty"); + put("id", "a-p-tag"); + }})); + widget.addWidget(new ShowIfWidget(targetWidgetChain, "false", evaluator)); + + expect(pageBook.forName(pageName)) + .andReturn(page); + + + //mypage does? + final MyEmbeddedPage myEmbeddedPage = new MyEmbeddedPage(); + expect(page.instantiate()) + .andReturn(myEmbeddedPage); + + expect(page.doMethod(isA(String.class), anyObject(), isA(String.class), + isA(HttpServletRequest.class))) + .andReturn(null); + expect(page.widget()) + .andReturn(widget); + + replay(pageBook, page); + + + final EmbedWidget embedWidget = new EmbedWidget(Collections.emptyMap(), expression, evaluator, pageBook, pageName); + embedWidget.init(new EmbeddedRespondFactory(RespondersForTesting.newRespond()), HtmlTemplateCompilerTest.mockRequestProviderForContext()); + embedWidget + .render(new MyParentPage(passOn), respond); + + //assert bindings + assert myEmbeddedPage.isSet() : "variable not passed on to embedded page"; + assert passOn.equals(myEmbeddedPage.getMessage()) : "variable not set on embedded page"; + + //the render was ok + final String resp = respond.toString(); + assert "".equals(resp) : "widget not embedded correctly : " + resp; + + verify(pageBook, page); + } + + + @Test(dataProvider = PAGES_FOR_EMBEDDING) + public final void pageEmbeddingAndBinding(String pageName, final String passOn, final String expression) { + //forName expects pageName to be in all-lower case (it's an optimization) + pageName = pageName.toLowerCase(); + + final PageBook pageBook = createMock(PageBook.class); + final PageBook.Page page = createMock(PageBook.Page.class); + final Respond mockRespond = createNiceMock(Respond.class); + final Renderable widget = createMock(Renderable.class); + + expect(pageBook.forName(pageName)) + .andReturn(page); + + + //mypage does? + final MyEmbeddedPage myEmbeddedPage = new MyEmbeddedPage(); + expect(page.instantiate()) + .andReturn(myEmbeddedPage); + + + expect(page.doMethod(isA(String.class), anyObject(), isA(String.class), + isA(HttpServletRequest.class))) + .andReturn(null); + expect(page.widget()) + .andReturn(widget); + + widget.render(eq(myEmbeddedPage), isA(Respond.class)); + + + replay(pageBook, page, mockRespond, widget); + + final EmbedWidget embedWidget = new EmbedWidget(Collections.emptyMap(), expression, new MvelEvaluator(), pageBook, pageName); + embedWidget.init(new EmbeddedRespondFactory(RespondersForTesting.newRespond()), HtmlTemplateCompilerTest.mockRequestProviderForContext()); + embedWidget + .render(new MyParentPage(passOn), mockRespond); + + + assert myEmbeddedPage.isSet() : "variable not passed on to embedded page"; + assert passOn.equals(myEmbeddedPage.getMessage()) : "variable not set on embedded page"; + + verify(pageBook, page, mockRespond, widget); + } + + @Test(dataProvider = PAGES_FOR_EMBEDDING_BROKEN_EXCEPTION, expectedExceptions = IllegalArgumentException.class) + public final void failedPageEmbeddingThrowing(String pageName, final String passOn, final String expression) { + //forName expects pageName to be in all-lower case (it's an optimization) + pageName = pageName.toLowerCase(); + + final PageBook pageBook = createMock(PageBook.class); + final PageBook.Page page = createMock(PageBook.Page.class); + final Respond mockRespond = createMock(Respond.class); + final Renderable widget = createMock(Renderable.class); + + expect(pageBook.forName(pageName)) + .andReturn(page); + + + //mypage does? + final MyEmbeddedPage myEmbeddedPage = new MyEmbeddedPage(); + expect(page.instantiate()) + .andReturn(myEmbeddedPage); + + expect(page.doMethod(isA(String.class), anyObject(), isA(String.class), + isA(HttpServletRequest.class))) + .andReturn(null); + + expect(page.widget()) + .andReturn(widget); + + widget.render(myEmbeddedPage, mockRespond); + + + replay(pageBook, page, mockRespond, widget); + + new EmbedWidget(Collections.emptyMap(), expression, new MvelEvaluator(), pageBook, pageName) + .render(new MyParentPage(passOn), mockRespond); + + +// assert !passOn.equals(myEmbeddedPage.getMessage()) : "variable somehow set on embedded page"; + + verify(pageBook, page, mockRespond, widget); + } + + @Test(dataProvider = PAGES_FOR_EMBEDDING_BROKEN) + public final void failedPageEmbedding(String pageName, final String passOn, final String expression) { + //forName expects pageName to be in all-lower case (it's an optimization) + pageName = pageName.toLowerCase(); + + final PageBook pageBook = createMock(PageBook.class); + final PageBook.Page page = createMock(PageBook.Page.class); + final Respond mockRespond = createNiceMock(Respond.class); //tolerate whatever output + final Renderable widget = createMock(Renderable.class); + + expect(pageBook.forName(pageName)) + .andReturn(page); + + + //mypage does? + final MyEmbeddedPage myEmbeddedPage = new MyEmbeddedPage(); + expect(page.instantiate()) + .andReturn(myEmbeddedPage); + + expect(page.doMethod(isA(String.class), anyObject(), isA(String.class), + isA(HttpServletRequest.class))) + .andReturn(null); + expect(page.widget()) + .andReturn(widget); + + widget.render(eq(myEmbeddedPage), isA(Respond.class)); + + + replay(pageBook, page, mockRespond, widget); + + final EmbedWidget embedWidget = new EmbedWidget(Collections.emptyMap(), expression, new MvelEvaluator(), pageBook, pageName); + embedWidget.init(new EmbeddedRespondFactory(RespondersForTesting.newRespond()), HtmlTemplateCompilerTest.mockRequestProviderForContext()); + embedWidget + .render(new MyParentPage(passOn), mockRespond); + + + assert !passOn.equals(myEmbeddedPage.getMessage()) : "variable somehow set on embedded page"; + + verify(pageBook, page, mockRespond, widget); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/control/EmbeddedRespondExtractorTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/EmbeddedRespondExtractorTest.java new file mode 100644 index 00000000..aeaad40d --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/EmbeddedRespondExtractorTest.java @@ -0,0 +1,140 @@ +package com.google.sitebricks.rendering.control; + +import com.google.inject.Guice; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.Collections; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class EmbeddedRespondExtractorTest { + private static final String HTMLDOCS_AND_SPLITS = "htmlDocsNSplitz"; + + + + public static final String HTML_DOC_SIMPLE = "\n" + + " @Meta\n" + + " \n" + + " yoyo\n" + + " \n" + + " \n" + + " \n" + + "\n\n" + + "

Greetings

\n\n" + + " " + + " Some free text\n" + + ""; + + public static final String HTML_DOC_BODY_WITHATTRS = "\n" + + " @Meta\n" + + " \n" + + " yoyo\n" + + " \n" + + " \n" + + " \n" + + "\n\n" + + "

Greetings

\n\n" + + " " + + " Some free text\n" + + ""; + + public static final String HTML_DOC_BODY_WITHATTRS_INQUOTES = "\n" + + " @Meta\n" + + " \n" + + " yoyo\n" + + " \n" + + " \n" + + " jebious')\" >\n" + + "\n\n" + + "

Greetings

\n\n" + + " " + + " Some free text\n" + + ""; + + public static final String HTML_DOC_HEAD_WITHATTRS_INQUOTES = "\n" + + " @Meta\n" + + " jebious')\">\n" + + " yoyo\n" + + " \n" + + " \n" + + " \n" + + "\n\n" + + "

Greetings

\n\n" + + " " + + " Some free text\n" + + ""; + + + public static final String HTML_DOC_BODY_WITHATTRS_MESSY_QUOTE = "\n" + + " @Meta\n" + + " >>>\">\n" + + " yoyo\n" + + " \n" + + " \n" + + " @Frame\n" + + " \n" + + "\n\n" + + "

Greetings

\n\n" + + " @TextField" + + " " + + " Some free text\n" + + ""; + + public static final String HTML_DOC_HEADLESS = "\n" + + " @Meta\n" + + " \n" + + " \n" + + " \n" + + "\n\n" + + "

Greetings

\n\n" + + " " + + " Some free text\n" + + ""; + + public static final String HTML_DOC_SELF_CLOSED_HEAD = "\n" + + " @Meta\n" + + " \n" + + " \n" + + " \n" + + "\n\n" + + "

Greetings

\n\n" + + " " + + " Some free text\n" + + ""; + + @DataProvider(name = HTMLDOCS_AND_SPLITS) + public Object[][] get() { + return new Object[][] { + {HTML_DOC_SIMPLE, "yoyo", "

Greetings

\n\n Some free text" }, + {HTML_DOC_BODY_WITHATTRS, "yoyo", "

Greetings

\n\n Some free text" }, + {HTML_DOC_HEAD_WITHATTRS_INQUOTES, "yoyo", "

Greetings

\n\n Some free text" }, + {HTML_DOC_BODY_WITHATTRS_INQUOTES, "yoyo", "

Greetings

\n\n Some free text" }, + {HTML_DOC_BODY_WITHATTRS_MESSY_QUOTE, "yoyo", "

Greetings

\n\n @TextField Some free text" }, + {HTML_DOC_HEADLESS, "", "

Greetings

\n\n Some free text" }, + {HTML_DOC_SELF_CLOSED_HEAD, "", "

Greetings

\n\n Some free text" }, + }; + } + + @Test(dataProvider = HTMLDOCS_AND_SPLITS) + public final void extractInsideHeadTags(final String htmlDoc, String expectedHead, String expectedBody) { + final EmbeddedRespondFactory factory = Guice.createInjector().getInstance(EmbeddedRespondFactory.class); + + final EmbeddedRespond respond = factory.get(Collections.emptyMap()); + + respond.write(htmlDoc); + + final String head = respond.toHeadString(); + final String body = respond.toString(); + + assert null != head : "Head was null"; + assert null != body : "body was null"; + +// System.out.println("head: " + head); +// System.out.println("body: " + body); +// assert "".equals(head.trim()) : "Head did not match : " + head; + assert expectedBody.equals(body.trim()) : "Body did not match"; + + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/control/HeaderWidgetTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/HeaderWidgetTest.java new file mode 100644 index 00000000..1b5634f4 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/HeaderWidgetTest.java @@ -0,0 +1,65 @@ +package com.google.sitebricks.rendering.control; + +import com.google.common.collect.Maps; +import com.google.sitebricks.Respond; +import com.google.sitebricks.RespondersForTesting; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.MvelEvaluatorCompiler; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class HeaderWidgetTest { + private static final String EXPRESSIONS_AND_EVALS = "expressionsAndEvals"; + + @DataProvider(name = EXPRESSIONS_AND_EVALS) + public Object[][] getExprs() { + return new Object[][]{ + {"visible", true}, + {"!visible", false}, + {"true", true}, + {"false", false}, + }; + } + + @Test + public final void renderHeader() throws ExpressionCompileException { + + Respond respond = RespondersForTesting.newRespond(); + + MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(Object.class); + new HeaderWidget(new ProceedingWidgetChain(), Maps.newHashMap(), compiler) + .render(new Object(), respond); + + respond.writeToHead("bs"); + + final String response = respond.toString(); + assert "bs".equals(response) : + "instead printed: " + response; + } + + @Test + public final void renderHeaderWithContent() throws ExpressionCompileException { + + Respond respond = RespondersForTesting.newRespond(); + + final WidgetChain widgetChain = new ProceedingWidgetChain(); + final EvaluatorCompiler mock = new MvelEvaluatorCompiler(Object.class); + widgetChain.addWidget(new TextWidget("", mock)); + + MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(Object.class); + + new HeaderWidget(widgetChain, Maps.newHashMap(), compiler) + .render(new Object(), respond); + + respond.writeToHead("bs"); + + final String response = respond.toString(); + assert "bs".equals(response) : + "instead printed: " + response; + } +} \ No newline at end of file diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/control/RepeatWidgetTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/RepeatWidgetTest.java new file mode 100644 index 00000000..ad5eb88a --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/RepeatWidgetTest.java @@ -0,0 +1,117 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Evaluator; +import com.google.sitebricks.MvelEvaluator; +import com.google.sitebricks.Respond; +import com.google.sitebricks.RespondersForTesting; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.conversion.DummyTypeConverter; +import com.google.sitebricks.conversion.TypeConverter; +import com.google.sitebricks.rendering.DynTypedMvelEvaluatorCompiler; + +import org.mvel2.optimizers.OptimizerFactory; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class RepeatWidgetTest { + private static final String LISTS_AND_TIMES = "listsAndTimes"; + private static final String EXPRS_AND_OBJECTS = "exprsNObjs"; + + private static final String A_NAME = "Dhanji"; + + @DataProvider(name = LISTS_AND_TIMES) + public Object[][] getlistsAndTimes() { + return new Object[][] { + { 5, Arrays.asList(1,2,3,4,4) }, + { 4, Arrays.asList(1,2,3,4) }, + { 16, Arrays.asList(1,2,3,2,2,2,2,1,2,1,2,1,2,1,2,2) }, + { 0, Arrays.asList() }, + }; + } + + + @Test(dataProvider = LISTS_AND_TIMES) + public final void repeatNumberOfTimes(int should, final Collection ints) { + + final int[] times = new int[1]; + final WidgetChain mockChain = new ProceedingWidgetChain() { + @Override + public void render(Object bound, Respond respond) { + times[0]++; + } + }; + + + RepeatWidget widget = new RepeatWidget(mockChain, "items=beans", new MvelEvaluator()); + widget.setConverter(new DummyTypeConverter()); + widget.render(new HashMap() {{ + put("beans", ints); + }}, RespondersForTesting.newRespond()); + + + + + assert times[0] == should : "Did not run expected number of times: " + should; + } + + @DataProvider(name = EXPRS_AND_OBJECTS) + public Object[][] getExpressionsAndObjects() { + return new Object[][] { + { "items=things, var='thing', pageVar='page'", new HashMap() {{ + put("things", Arrays.asList(new Thing(), new Thing(), new Thing())); + }}, 3, "thing" + }, + { "items=things, pageVar='page'", new HashMap() {{ + put("things", Arrays.asList(new Thing(), new Thing(), new Thing())); + }}, 3, "__this" + }, + { "items=things, var='thingy', pageVar='page'", new HashMap() {{ + put("things", Arrays.asList(new Thing(), new Thing(), new Thing())); + }}, 3, "thingy" + } + }; + } + +// @Test(dataProvider = EXPRS_AND_OBJECTS) + public final void repeatNumberOfTimesWithVars(String expression, Object page, int should, final String exp) throws ExpressionCompileException { + OptimizerFactory.setDefaultOptimizer(OptimizerFactory.SAFE_REFLECTIVE); + final int[] times = new int[1]; + final Evaluator evaluator = new DynTypedMvelEvaluatorCompiler(null).compile(exp); + final WidgetChain mockChain = new ProceedingWidgetChain() { + @Override + public void render(final Object bound, Respond respond) { + times[0]++; + + final Object thing = evaluator.evaluate(exp, bound); + assert thing instanceof Thing : "Contextual (current) var not set: " + thing; + assert A_NAME.equals(((Thing)thing).getName()); + } + }; + + + new RepeatWidget(mockChain, expression, evaluator) + .render(page, RespondersForTesting.newRespond()); + + assert times[0] == should : "Did not run expected number of times: " + should; + } + + public static class Thing { + private String name = A_NAME; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/control/RequireWidgetTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/RequireWidgetTest.java new file mode 100644 index 00000000..5ac3c895 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/RequireWidgetTest.java @@ -0,0 +1,51 @@ +package com.google.sitebricks.rendering.control; + +import com.google.common.collect.Maps; +import com.google.sitebricks.Respond; +import com.google.sitebricks.RespondersForTesting; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.MvelEvaluatorCompiler; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class RequireWidgetTest { + private static final String REQUIRE_TAGS = "requireTags"; + + @DataProvider(name = REQUIRE_TAGS) + public Object[][] getRequires() { + return new Object[][]{ + {""}, + {""}, + }; + } + + @Test(dataProvider = REQUIRE_TAGS) + public final void requireWidgetsRenderToHeadTag(final String requireString) + throws ExpressionCompileException { + final Respond respond = RespondersForTesting.newRespond(); + + respond.require(requireString); + respond.require(requireString); + + WidgetChain chain = new ProceedingWidgetChain(); + final MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(Object.class); + + chain.addWidget(new HeaderWidget(new TerminalWidgetChain(), + Maps.newHashMap(), compiler)); + + chain.addWidget(new RequireWidget(requireString, compiler)); + chain.addWidget(new RequireWidget(requireString, compiler)); + chain.addWidget(new RequireWidget(requireString, compiler)); + + //render + chain.render(new Object(), respond); + + final String expected = "" + requireString + ""; + final String output = respond.toString(); + assert expected.equals(output) : "Header not correctly rendered: " + output; + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/control/ShowIfWidgetTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/ShowIfWidgetTest.java new file mode 100644 index 00000000..304e73f6 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/ShowIfWidgetTest.java @@ -0,0 +1,46 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.MvelEvaluator; +import com.google.sitebricks.Respond; +import com.google.sitebricks.RespondersForTesting; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.HashMap; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class ShowIfWidgetTest { + private static final String EXPRESSIONS_AND_EVALS = "expressionsAndEvals"; + + @DataProvider(name = EXPRESSIONS_AND_EVALS) + public Object[][] getExprs() { + return new Object[][] { + { "visible", true }, + { "!visible", false }, + { "true", true }, + { "false", false }, + }; + } + + @Test(dataProvider = EXPRESSIONS_AND_EVALS) + public final void hideChildren(String expression, boolean should) { + final boolean[] run = new boolean[1]; + WidgetChain mockChain = new ProceedingWidgetChain() { + @Override + public void render(Object bound, Respond respond) { + run[0] = true; + } + }; + + + //try to render widget + new ShowIfWidget(mockChain, expression, new MvelEvaluator()) + .render(new HashMap() {{ + put("visible", true); + }}, RespondersForTesting.newRespond()); + + assert run[0] == should : "ShowIf did not do as it should " + should; + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/control/TextFieldWidgetTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/TextFieldWidgetTest.java new file mode 100644 index 00000000..8fcf81dd --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/TextFieldWidgetTest.java @@ -0,0 +1,37 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.MvelEvaluator; +import com.google.sitebricks.Respond; +import static org.easymock.EasyMock.createMock; + +import java.util.HashMap; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class TextFieldWidgetTest { + +// @Test + public final void textTagRender() { + + final String[] out = new String[1]; + Respond mockRespond = createMock(Respond.class); + final String boundTo = "aString"; + + new TextFieldWidget(new ProceedingWidgetChain(), "boundTo", new MvelEvaluator()) + .render(new HashMap() {{ + put("boundTo", boundTo); + }}, mockRespond); + + + //assert the validity of the text tag: + assert out[0] != null : "Nothing rendered!"; + String tag = out[0].trim(); + + assert tag.startsWith(""); + assert tag.contains("value=\"" + boundTo + "\""); + assert tag.contains("name=\"boundTo\""); + assert tag.contains("type=\"text\""); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/control/TextWidgetTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/TextWidgetTest.java new file mode 100644 index 00000000..2746bbd4 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/TextWidgetTest.java @@ -0,0 +1,110 @@ +package com.google.sitebricks.rendering.control; + +import com.google.sitebricks.Respond; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.compiler.MvelEvaluatorCompiler; +import static org.easymock.EasyMock.*; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class TextWidgetTest { + private static final String NAME_VALUES = "nameValues"; + private static final String MVEL_NAMES = "mvelNames"; + + @DataProvider(name = NAME_VALUES) + Object[][] getNameValues() { + return new Object[][] { + { "Dhanji" }, + { "Joe" }, + { "Josh" }, + }; + } + + @DataProvider(name = MVEL_NAMES) //creates a path for expr: ${names.first} + Object[][] getMvelNames() { + return new Object[][] { + { new TestBackingType(new ANestedType("Dhanji", "NotDhanji")), "Dhanji" }, + { new TestBackingType(new ANestedType("Joei", "NotDhanji")), "Joei" }, + { new TestBackingType(new ANestedType("Jill", "NotDhanji")), "Jill" }, + + + }; + } + + public static class TestBackingType { + private ANestedType names; + + public TestBackingType(ANestedType names) { + this.names = names; + } + + public ANestedType getNames() { + return names; + } + } + + public static class ANestedType { + private String first; + private String second; + + public ANestedType(String first, String second) { + this.first = first; + this.second = second; + } + + public String getFirst() { + return first; + } + + public String getSecond() { + return second; + } + } + + @Test(dataProvider = NAME_VALUES) + public final void renderATemplateWithObject(final String name) throws ExpressionCompileException { + final String[] out = new String[1]; + Respond respond = createMock(Respond.class); + respond.write("Hello " + name); + + + replay(respond); + + new TextWidget("Hello ${name}", new MvelEvaluatorCompiler(ATestType.class)) + .render(new ATestType(name), respond); + +// assert ("Hello " + name).equals(out[0]) : "template render failed: " + out[0]; + verify(respond); + } + + public static class ATestType { + private String name; + + public ATestType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + @Test(dataProvider = MVEL_NAMES) + public final void renderATemplateWithObjectGraph(final TestBackingType data, String name) throws ExpressionCompileException { + final String[] out = new String[1]; + Respond respond = createMock(Respond.class); + + respond.write("Hello " + name); + + replay(respond); + + new TextWidget("Hello ${names.first}", new MvelEvaluatorCompiler(TestBackingType.class)) + .render(data, respond); + +// assert ("Hello " + name).equals(out[0]) : "template render failed: " + out[0]; + verify(respond); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/control/WidgetRegistryTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/WidgetRegistryTest.java new file mode 100644 index 00000000..a4ef66a3 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/control/WidgetRegistryTest.java @@ -0,0 +1,38 @@ +package com.google.sitebricks.rendering.control; + +import com.google.inject.Injector; +import com.google.sitebricks.MvelEvaluator; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.compiler.EvaluatorCompiler; +import com.google.sitebricks.compiler.ExpressionCompileException; +import com.google.sitebricks.routing.PageBook; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.createNiceMock; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class WidgetRegistryTest { + private static final String WIDGETS_AND_KEYS = "widgetsAndKeys"; + + @DataProvider(name = WIDGETS_AND_KEYS) + public Object[][] get() { + return new Object[][] { + { "twidg", TextFieldWidget.class }, + { "teasdasxt", RepeatWidget.class }, + { "sastext", ShowIfWidget.class }, + }; + } + + @Test(dataProvider = WIDGETS_AND_KEYS) + public final void storeRetrieveWidgets(final String key, final Class expected) throws ExpressionCompileException { + final WidgetRegistry registry = new DefaultWidgetRegistry(new MvelEvaluator(), createNiceMock(PageBook.class), createNiceMock(Injector.class)); + registry.add(key, expected); + + Renderable widget = registry.newWidget(key, "some=expression", new ProceedingWidgetChain(), createMock(EvaluatorCompiler.class)); + + assert expected.isInstance(widget) : "Wrong widget returned"; + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/resource/MimeTypesRegexIntegrationTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/resource/MimeTypesRegexIntegrationTest.java new file mode 100644 index 00000000..32dfdfd2 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/rendering/resource/MimeTypesRegexIntegrationTest.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.rendering.resource; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class MimeTypesRegexIntegrationTest { + private static final String MIMES_AND_FILES = "mimesAndFiles"; + + @DataProvider(name = MIMES_AND_FILES) + public Object[][] get() { + return new Object[][] { + { "/thing/holy.js", "text/javascript" }, + { "/thing/%20blah.thingaly.xml", "text/xml" }, + { "/thing/%20blah.thingalyxml", "text/plain" }, //default + { "/thing/holy.js/nekkid.png", "image/png" }, + }; + } + + @Test(dataProvider = MIMES_AND_FILES) + public final void mimeTypeMatching(final String file, final String mimeType) throws IOException { + new ClasspathResourcesService(); //impure call loads mimetypes from classpath + + final String mime = ClasspathResourcesService.mimeOf(file); + assert mimeType.equals(mime) : "Did not match, instead was: " + mime; + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/rendering/resource/ResourcesServiceTest.java b/sitebricks/src/test/java/com/google/sitebricks/rendering/resource/ResourcesServiceTest.java new file mode 100644 index 00000000..e69de29b diff --git a/sitebricks/src/test/java/com/google/sitebricks/routing/PageBookImplTest.java b/sitebricks/src/test/java/com/google/sitebricks/routing/PageBookImplTest.java new file mode 100644 index 00000000..3d3773e6 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/routing/PageBookImplTest.java @@ -0,0 +1,732 @@ +package com.google.sitebricks.routing; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.name.Named; +import com.google.sitebricks.At; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.SitebricksModule; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; +import com.google.sitebricks.http.Select; +import com.google.sitebricks.rendering.EmbedAs; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class PageBookImplTest { + private static final String FIRST_PATH_ELEMENTS = "firstPathElements"; + private static final String URI_TEMPLATES_AND_MATCHES = "uriTemplatesAndMatches"; + private static final String NOT_URIS_AND_TEMPLATES = "noturisandTemplates"; + private static final String REDIRECTED_GET = "/redirected_get"; + private static final String REDIRECTED_POST = "/redirected_post"; + + private Injector injector; + + @BeforeTest + public final void pre() { + injector = Guice.createInjector(new SitebricksModule()); + } + + @Test + public final void storeAndRetrievePageInstance() { + final Respond respond = new MockRespond(); + + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki", MyPage.class); + + PageBook.Page page = pageBook.get("/wiki"); + page.apply(mock); + page.widget().render(new Object(), respond); + + assert page.widget().equals(mock); + } + + @Test + public final void fireGetMethodOnPage() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki", MyPage.class); + + PageBook.Page page = pageBook.get("/wiki"); + page.apply(mock); + final MyPage bound = new MyPage(); + page.doMethod("get", bound, "/wiki", fakeRequestWithParams(new HashMap())); + + assert page.widget().equals(mock); + assert bound.getted : "@Get method was not fired, on doGet()"; + } + + @Test + public final void fireGetMethodOnPageAndRedirectToURL() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki", MyRedirectingPage.class); + + PageBook.Page page = pageBook.get("/wiki"); + page.apply(mock); + final MyRedirectingPage bound = new MyRedirectingPage(); + Object redirect = page.doMethod("get", bound, "/wiki", fakeRequestWithParams(new HashMap())); + + assert REDIRECTED_GET.equals(redirect); + assert page.widget().equals(mock); + } + + @Test + public final void firePostMethodOnPageAndRedirectToURL() { + Renderable mock = new + Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki", MyRedirectingPage.class); + + PageBook.Page page = pageBook.get("/wiki"); + page.apply(mock); + final MyRedirectingPage bound = new MyRedirectingPage(); + Object redirect = page.doMethod("post", bound, "/wiki", fakeRequestWithParams(new HashMap())); + + assert REDIRECTED_POST.equals(redirect); + assert page.widget().equals(mock); + } + + @Test + public final void fireGetMethodOnPageToCorrectHandler() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + Map params = new HashMap() { + { + put("event", new String[]{"1", "2"}); + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki", MyEventSupportingPage.class); + + PageBook.Page page = pageBook.get("/wiki"); + page.apply(mock); + final MyEventSupportingPage bound = new MyEventSupportingPage(); + page.doMethod("get", bound, "/wiki", fakeRequestWithParams(params)); + + assert page.widget().equals(mock); + assert bound.getted1 : "@Get @On method was not fired, on doGet() for [event=1]"; + assert bound.getted2 : "@Get @On method was not fired, on doGet() for [event=2]"; + } + + @Test + public final void firePostMethodOnPageToCorrectHandler() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + Map params = new HashMap() { + { + put("event", new String[]{"1", "2"}); + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki", MyEventSupportingPage.class); + + PageBook.Page page = pageBook.get("/wiki"); + page.apply(mock); + final MyEventSupportingPage bound = new MyEventSupportingPage(); + page.doMethod("post", bound, "/wiki", fakeRequestWithParams(params)); + + assert page.widget().equals(mock); + assert bound.posted1 : "@Post @On method was not fired, on doPost() for [event=1]"; + assert bound.posted2 : "@Post @On method was not fired, on doPost() for [event=2]"; + } + + @Test + public final void fireGetMethodOnPageToCorrectHandlerOnlyOnce() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + Map params = new HashMap() { + { + put("event", new String[]{"2"}); + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki", MyEventSupportingPage.class); + + PageBook.Page page = pageBook.get("/wiki"); + page.apply(mock); + final MyEventSupportingPage bound = new MyEventSupportingPage(); + page.doMethod("get", bound, "/wiki", fakeRequestWithParams(params)); + + assert page.widget().equals(mock); + assert !bound.getted1 : "@Get @On method was fired, on doGet() for [event=1]"; + assert bound.getted2 : "@Get @On method was not fired, on doGet() for [event=2]"; + } + + @Test + public final void firePostMethodOnPageToCorrectHandlerOnlyOnce() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + Map params = new HashMap() { + { + put("event", new String[]{"2"}); + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki", MyEventSupportingPage.class); + + PageBook.Page page = pageBook.get("/wiki"); + page.apply(mock); + final MyEventSupportingPage bound = new MyEventSupportingPage(); + page.doMethod("post", bound, "/wiki", fakeRequestWithParams(params)); + + assert page.widget().equals(mock); + assert !bound.posted1 : "@Post @On method was fired, on doGet() for [event=1]"; + assert bound.posted2 : "@Post @On method was not fired, on doGet() for [event=2]"; + } + + @Test + public final void fireGetMethodOnPageToDefaultHandler() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + Map params = new HashMap() { + { + put("event", new String[]{"3"}); + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki", MyEventSupportingPage.class); + + PageBook.Page page = pageBook.get("/wiki"); + page.apply(mock); + final MyEventSupportingPage bound = new MyEventSupportingPage(); + page.doMethod("get", bound, "/wiki", fakeRequestWithParams(params)); + + assert page.widget().equals(mock); + assert !bound.getted1 : "@Get @On method was fired, on doGet() for [event=1]"; + assert !bound.getted2 : "@Get @On method was fired, on doGet() for [event=2]"; + assert bound.defaultGet : "@Get @On default method was not fired, on doGet() for [event=...]"; + + } + + + @Test + public final void firePostMethodOnPageToDefaultHandler() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + Map params = new HashMap() { + { + put("event", new String[]{"3"}); + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki", MyEventSupportingPage.class); + + PageBook.Page page = pageBook.get("/wiki"); + page.apply(mock); + final MyEventSupportingPage bound = new MyEventSupportingPage(); + page.doMethod("post", bound, "/wiki", fakeRequestWithParams(params)); + + assert page.widget().equals(mock); + assert !bound.getted2 : "@Get @On method was fired, on doPost() for [event=2]"; + assert !bound.getted1 : "@Get @On method was fired, on doPost() for [event=1]"; + assert !bound.posted1 : "@Post @On method was fired, on doPost() for [event=1]"; + assert !bound.posted2 : "@Post @On method was fired, on doPost() for [event=2]"; + assert bound.defaultPost : "@Post @On default method was not fired, on doPost() for [event=...]"; + + } + + @Test + public final void fireGetMethodWithArgsOnPage() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki/:title", MyPageWithTemplate.class); + + PageBook.Page page = pageBook.get("/wiki/IMAX"); + page.apply(mock); + final MyPageWithTemplate bound = new MyPageWithTemplate(); + page.doMethod("get", bound, "/wiki/IMAX", fakeRequestWithParams(new HashMap())); + + assert page.widget().equals(mock); + assert "IMAX".equals(bound.title) : "@Get method was not fired, on doGet() with the right arg, instead: " + bound.title; + } + + + @Test + public final void fireGetMethodWithPrimitiveArgsOnPage() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + Date date = new Date(); +// SimpleDateFormat sdf = new SimpleDateFormat(StringToDateTimeCalendarConverter.USA_SHORT); + + final PageBook pageBook = new DefaultPageBook(injector); +// pageBook.at("/wiki/:title/cat/:int/:bool/:float/:date", MyPageWithPrimitivesTemplate.class); + pageBook.at("/wiki/:title/cat/:int/:bool/:float", MyPageWithPrimitivesTemplate.class); + + String targetURL = "/wiki/IMAX/cat/1/true/2.5";; + PageBook.Page page = pageBook.get(targetURL); + page.apply(mock); + final MyPageWithPrimitivesTemplate bound = new MyPageWithPrimitivesTemplate(); + page.doMethod("get", bound, targetURL, fakeRequestWithParams(new HashMap())); + + assert page.widget().equals(mock); + assert "IMAX".equals(bound.title) && bound.id == 1 && bound.bool == true && bound.flt == 2.5 +// && sdf.format(date).equals(sdf.format(bound.date)): + : "@Get method did not bind in args correctly, title: " + bound.title + +// " id: " + bound.id + " bool: " + bound.bool + " float: " + bound.flt + " date: " + sdf.format(bound.date); + " id: " + bound.id + " bool: " + bound.bool + " float: " + bound.flt; + ; + } + + + @Test + public final void firePostMethodWithArgsOnPage() { + Renderable mock = new + Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki/:title/cat/:id", MyPageWithTemplate.class); + + PageBook.Page page = pageBook.get("/wiki/IMAX_P/cat/12"); + page.apply(mock); + final MyPageWithTemplate bound = new MyPageWithTemplate(); + page.doMethod("post", bound, "/wiki/IMAX_P/cat/12", fakeRequestWithParams(new HashMap())); + + assert page.widget().equals(mock); + assert "IMAX_P".equals(bound.post) && "12".equals(bound.id) + : "@Post method was not fired, on doPost() with the right arg, instead: " + bound.post; + } + + @Test(expectedExceptions = InvalidEventHandlerException.class) + public final void errorOnPostMethodWithUnnamedArgs() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki/:title/cat/:id", MyBrokenPageWithTemplate.class); + + PageBook.Page page = pageBook.get("/wiki/IMAX_P/cat/12"); + final MyBrokenPageWithTemplate bound = new MyBrokenPageWithTemplate(); + page.doMethod("post", bound, "/wiki/IMAX_P/cat/12", fakeRequestWithParams(new HashMap())); + + assert page.widget().equals(mock); + } + + @Test + public final void firePostMethodOnPage() { + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at("/wiki", MyPage.class); + + PageBook.Page page = pageBook.get("/wiki"); + final MyPage bound = new MyPage(); + page.apply(mock); + page.doMethod("post", bound, "/wiki", fakeRequestWithParams(new HashMap())); + + assert page.widget().equals(mock); + assert bound.posted : "@Post method was not fired, on doPost()"; + } + + @DataProvider(name = URI_TEMPLATES_AND_MATCHES) + public Object[][] getUriTemplatesAndMatches() { + return new Object[][]{ + {"/wiki/:title", "/wiki/HelloPage"}, + {"/wiki/:title", "/wiki/HelloPage%20"}, + {"/wiki/:title/dude", "/wiki/HelloPage/dude"}, + {"/:title/thing", "/wiki/thing"}, + {"/:title", "/aposkdapoksd"}, + }; + } + + @Test(dataProvider = URI_TEMPLATES_AND_MATCHES) + public final void matchPageByUriTemplate(final String template, final String toMatch) { + final Respond respond = new MockRespond(); + + Renderable mock = new Renderable() { + public void render(Object bound, Respond respond) { + + } + + public Set collect(Class clazz) { + return null; + } + }; + + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at(template, MyPage.class); + + PageBook.Page page = pageBook.get(toMatch); + final MyPage myPage = new MyPage(); + + page.apply(mock); + page.widget().render(myPage, respond); + + assert mock.equals(page.widget()); + } + + @DataProvider(name = NOT_URIS_AND_TEMPLATES) + public Object[][] getNotUriTemplatesAndMatches() { + return new Object[][]{ + {"/wiki/:title", "/tiki/HelloPage"}, + {"/wiki/:title", "/wiki/HelloPage%20/didle"}, + {"/wiki/:title/dude", "/wiki/HelloPage"}, + {"/:title/thing", "/wiki/thing/thingaling"}, + {"/:title", "/aposkdapoksd/12"}, + }; + } + + @Test(dataProvider = NOT_URIS_AND_TEMPLATES) + public final void notMatchPageByUriTemplate(final String template, final String toMatch) { + final PageBook pageBook = new DefaultPageBook(injector); + pageBook.at(template, MyPage.class); + + //cant find + assert null == pageBook.get(toMatch); + + } + + public static HttpServletRequest fakeRequestWithParams(Map map) { + HttpServletRequest request = createMock(HttpServletRequest.class); + + expect(request.getParameterMap()).andReturn(map); + replay(request); + + return request; + } + + @At("/wiki") + @Select("event") + public static class MyEventSupportingPage { + private boolean getted1; + private boolean getted2; + private boolean posted1; + private boolean posted2; + private boolean defaultGet; + private boolean defaultPost; + + @Get("1") + public void get1() { + getted1 = true; + } + + @Get("2") + public void get2() { + getted2 = true; + } + + @Get + public void defaultGet() { + defaultGet = true; + } + + @Post("1") + public void post1() { + posted1 = true; + } + + @Post("2") + public void post2() { + posted2 = true; + } + + @Post + public void defaultPost() { + defaultPost = true; + } + + } + + @At("/wiki") + @EmbedAs("Hi") + public static class MyPage { + private boolean getted; + private boolean posted; + + @Get + public void get() { + getted = true; + } + + @Post + public void post() { + posted = true; + } + + } + + @At("/wiki/:title/cat/:id") + @EmbedAs("Hi") + public static class MyPageWithTemplate { + private String title; + private boolean posted; + private String post; + private String id; + + @Get + public void get(@Named("title") String title) { + this.title = title; + } + + @Post + public void post(@Named("title") String title, @Named("id") String id) { + this.post = title; + this.id = id; + } + } + + + @At("/wiki/:title/cat/:int/:bool/:float/:date") + @EmbedAs("Hi") + public static class MyPageWithPrimitivesTemplate { + private String title; + private int id; + private boolean bool; + private Float flt; + private Date date; + + @Get + public void get(@Named("title") String title, @Named("int") Integer id, @Named("bool") Boolean bool, +// @Named("float") float flt, @Named("date") Date date) { + @Named("float") float flt) { + this.title = title; + this.id = id; + this.bool = bool; + this.flt = flt; + } + + } + + + + @At("/wiki/:title/cat/:id") + @EmbedAs("Hi") + public static class MyBrokenPageWithTemplate { + + @Post + public void post(@Named("title") String title, int x, @Named("id") String id) { + } + + } + + @DataProvider(name = FIRST_PATH_ELEMENTS) + public Object[][] get() { + return new Object[][]{ + {"/wiki/:title", "wiki"}, + {"/wiki/:title/:thing", "wiki"}, + {"/wiki/other/thing/dude", "wiki"}, + {"/wiki", "wiki"}, + {"/wiki/", "wiki"}, + {"/", ""}, + }; + } + + @Test(dataProvider = FIRST_PATH_ELEMENTS) + public final void firstPathElement(final String uri, final String answer) { + final String fPath = new DefaultPageBook(injector) + .firstPathElement(uri); + + assert answer.equals(fPath) : "wrong path: " + fPath; + } + + private static class MockRespond implements Respond { + + public void write(String text) { + } + + public HtmlTagBuilder withHtml() { + throw new AssertionError(); + } + + public void write(char c) { + } + + public void chew() { + + } + + public void writeToHead(String text) { + + } + + public void require(String requireString) { + + } + + public void redirect(String to) { + + } + + public String getContentType() { + return null; + } + + public String getRedirect() { + return null; + } + + public Renderable include(String argument) { + return null; + } + + public String getHead() { + return null; + } + + @Override + public void clear() { + } + } + + @At("/wiki") + private class MyRedirectingPage { + + @Get + public String get() { + return REDIRECTED_GET; + } + + @Post + public String post() { + return REDIRECTED_POST; + } + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/routing/PathMatcherTest.java b/sitebricks/src/test/java/com/google/sitebricks/routing/PathMatcherTest.java new file mode 100644 index 00000000..8392be5b --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/routing/PathMatcherTest.java @@ -0,0 +1,149 @@ +package com.google.sitebricks.routing; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class PathMatcherTest { + private static final String EXACT_PATHS = "exactPaths"; + private static final String SINGLE_VAR_PATHS = "getSingleVarPaths"; + private static final String ANTI_VAR_PATHS = "antiVarPaths"; + private static final String VARPATHS_MATCHES = "varpathsMatches"; + private static final String VARPATHS_ANTIMATCHES = "varpaths_antimatches"; + + @DataProvider(name = EXACT_PATHS) + public Object[][] getExactPaths() { + return new Object[][] { + { "/wiki", "/wiki", }, + { "/wiki/pensylvania","/wiki/pensylvania", }, + { "/12", "/12", }, + { "/", "/", }, + }; + } + + @Test(dataProvider = EXACT_PATHS) + public final void matchExactUriPath(final String path, final String incoming) { + assert new PathMatcherChain.SimplePathMatcher(path) + .matches(incoming); + } + + @SuppressWarnings({"UnusedDeclaration"}) + @Test(dataProvider = EXACT_PATHS) + public final void matchGreedy(final String path, final String incoming) { + assert new PathMatcherChain.GreedyPathMatcher("ogog") + .matches(incoming); + } + + + @DataProvider(name = SINGLE_VAR_PATHS) + public Object[][] getVarPaths() { + return new Object[][] { + { "/wiki/:title", "/wiki/hello", }, + { "/wiki/:title", "/wiki/ashello", }, + { "/wiki/:title", "/wiki/hoolig An+*", }, + { "/wiki/:title/page/:id", "/wiki/hello/page/12", }, + { "/wiki/:title/page/:id", "/wiki/couwdury/page/12424", }, + { "/wiki/:title/page/:id", "/wiki/sokdoasd/page/aoskpaokda", }, + { "/wiki", "/wiki/", }, + { "/wiki/:title", "/wiki/hello/", }, + }; + } + + @Test(dataProvider = SINGLE_VAR_PATHS) + public final void matchPathTemplate(final String path, final String incoming) { + assert new PathMatcherChain(path) + .matches(incoming); + } + + + @DataProvider(name = VARPATHS_MATCHES) + public Object[][] getVarPathsAndMatches() { + return new Object[][] { + { "/wiki/:title", "/wiki/hello", new HashMap() {{ + put("title", "hello"); + }}, }, + { "/wiki/:title/:page/:id", "/wiki/hello/page/12", new HashMap() {{ + put("title", "hello"); + put("page", "page"); + put("id", "12"); + }}, }, + { "/wiki/:title/page/:id", "/wiki/sokdoasd/page/aoskpaokda", new HashMap() {{ + put("title", "sokdoasd"); + put("id", "aoskpaokda"); + }}, }, + }; + } + + @Test(dataProvider = VARPATHS_MATCHES) + public final void findMatchVariables(final String path, final String incoming, Map map) { + final Map stringMap = new PathMatcherChain(path) + .findMatches(incoming); + + assert null != stringMap; + assert stringMap.size() == map.size(); + for (Map.Entry entry : stringMap.entrySet()) { + assert map.containsKey(entry.getKey()); + assert map.get(entry.getKey()).equals(entry.getValue()); + } + } + + @DataProvider(name = VARPATHS_ANTIMATCHES) + public Object[][] getVarPathsAntiMatches() { + return new Object[][] { + { "/wiki/:title", "/wiki/hello", new HashMap() {{ + put("title", "hellol"); + }}, }, + { "/wiki/:title/:page/:id", "/wiki/hello/page/12", new HashMap() {{ + put("title", "hello"); + put("id", "12"); + }}, }, + { "/wiki/:title/page/:id", "/wiki/sokdoasd/page/aoskpaokda", new HashMap() {{ + put("title", "sokdoasd"); + put("id", "aoskpaokda"); + put("pid", "aoskpaokda"); + }}, }, + }; + } + + @Test(dataProvider = VARPATHS_ANTIMATCHES, expectedExceptions = AssertionError.class) + public final void notFindMatchVariables(final String path, final String incoming, Map map) { + final Map stringMap = new PathMatcherChain(path) + .findMatches(incoming); + + assert null != stringMap; + assert stringMap.size() == map.size(); + for (Map.Entry entry : stringMap.entrySet()) { + assert map.containsKey(entry.getKey()); + assert map.get(entry.getKey()).equals(entry.getValue()); + } + } + + @DataProvider(name = ANTI_VAR_PATHS) + public Object[][] getAntiVarPaths() { + return new Object[][] { + { "/wiki/:title", "/clicky/hello", }, + { "/wiki/:title/page/:id", "/wiki/hello/dago/12", }, + { "/wiki/:title/page/:id", "/wiki/couwdury/1/12424", }, + { "/wiki/:title/page/:id", "/wikit/sokdoasd/page/aoskpaokda", }, + { "/wiki/:title", "/wikia", }, + { "/wiki", "/", }, + { "/wiki/fencepost", "/", }, + { "/wiki/fencepost/stupid", "/", }, + { "/wiki/hicki", "/wiki", }, + { "/wiki/:title", "/wiki/", }, + { "/wiki/:hickory/dickory", "/wiki/dickory", }, + { "/wiki/:title", "/wiki/hello/bye", }, + }; + } + + @Test(dataProvider = ANTI_VAR_PATHS) + public final void notMatchPathTemplate(final String path, final String incoming) { + assert !new PathMatcherChain(path) + .matches(incoming); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/routing/WidgetRoutingDispatcherTest.java b/sitebricks/src/test/java/com/google/sitebricks/routing/WidgetRoutingDispatcherTest.java new file mode 100644 index 00000000..e65f1149 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/routing/WidgetRoutingDispatcherTest.java @@ -0,0 +1,456 @@ +package com.google.sitebricks.routing; + +import com.google.inject.Provider; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; +import com.google.sitebricks.binding.FlashCache; +import com.google.sitebricks.binding.RequestBinder; +import com.google.sitebricks.headless.HeadlessRenderer; +import com.google.sitebricks.rendering.resource.ResourcesService; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.createNiceMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.matches; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +public class WidgetRoutingDispatcherTest { + private static final String REDIRECTED_POST = "/redirect_post"; + private static final String REDIRECTED_GET = "/redirect_get"; + private static final String A_STATIC_RESOURCE_URI = "/not_thing"; + + private Provider flashCacheProvider; + private FlashCache flashCache; + + private HttpServletResponse response; + + @BeforeMethod + @SuppressWarnings("unchecked") + public final void initFlashCacheProvider() { + flashCacheProvider = createMock(Provider.class); + flashCache = createMock(FlashCache.class); + + expect(flashCacheProvider.get()) + .andReturn(flashCache).anyTimes(); + + replay(flashCacheProvider); + + response = createNiceMock(HttpServletResponse.class); + } + + @Test + public final void dispatchRequestAndRespondOnGet() throws IOException { + final HttpServletRequest request = createMock(HttpServletRequest.class); + PageBook pageBook = createMock(PageBook.class); + PageBook.Page page = createMock(PageBook.Page.class); + Renderable widget = createMock(Renderable.class); + final Respond respond = createMock(Respond.class); + RequestBinder binder = createMock(RequestBinder.class); + + Object pageOb = new Object(); + + + expect(request.getRequestURI()) + .andReturn("/thing") + .anyTimes(); + + expect(request.getContextPath()) + .andReturn("") + .anyTimes(); + + expect(request.getParameterMap()) + .andReturn(new HashMap()) + .anyTimes(); + + expect(pageBook.get("/thing")) + .andReturn(page); + + binder.bind(request, pageOb); + expectLastCall().once(); + + expect(page.isHeadless()) + .andReturn(false); + + expect(page.widget()) + .andReturn(widget); + + expect(page.instantiate()) + .andReturn(pageOb); + + expect(request.getMethod()) + .andReturn("GET"); + + expect(page.doMethod("get", pageOb, "/thing", request)) + .andReturn(null); + + + widget.render(pageOb, respond); + expectLastCall().once(); + + + replay(request, page, pageBook, widget, respond, binder); + + Respond out = new WidgetRoutingDispatcher(pageBook, binder, new Provider() { + public Respond get() { + return respond; + } + }, createNiceMock(ResourcesService.class), flashCacheProvider, + createNiceMock(HeadlessRenderer.class)).dispatch(request, response); + + + assert out == respond : "Did not respond correctly"; + + verify(request, page, pageBook, widget, respond, binder); + + } + + @Test + public final void dispatchRequestToCorrectEventHandlerOnGet() throws IOException { + final HttpServletRequest request = createMock(HttpServletRequest.class); + PageBook pageBook = createMock(PageBook.class); + PageBook.Page page = createMock(PageBook.Page.class); + Renderable widget = createMock(Renderable.class); + final Respond respond = createMock(Respond.class); + RequestBinder binder = createMock(RequestBinder.class); + + Object pageOb = new Object(); + + + expect(request.getRequestURI()) + .andReturn("/thing") + .anyTimes(); + + expect(request.getContextPath()) + .andReturn("") + .anyTimes(); + + expect(pageBook.get("/thing")) + .andReturn(page); + + binder.bind(request, pageOb); + expectLastCall().once(); + + + expect(page.isHeadless()) + .andReturn(false); + + expect(page.widget()) + .andReturn(widget); + + expect(page.instantiate()) + .andReturn(pageOb); + + expect(request.getMethod()) + .andReturn("GET"); + + final HashMap parameterMap = new HashMap(); + expect(request.getParameterMap()) + .andReturn(parameterMap) + .anyTimes(); + + expect(page.doMethod("get", pageOb, "/thing", request)) + .andReturn(null); + + widget.render(pageOb, respond); + expectLastCall().once(); + + + replay(request, page, pageBook, widget, respond, binder); + + Respond out = new WidgetRoutingDispatcher(pageBook, binder, new Provider() { + public Respond get() { + return respond; + } + }, createNiceMock(ResourcesService.class), flashCacheProvider, + createNiceMock(HeadlessRenderer.class)).dispatch(request, response); + + + assert out == respond : "Did not respond correctly"; + + verify(request, page, pageBook, widget, respond, binder); + + } + + + @Test + public final void dispatchRequestAndRespondOnPost() throws IOException { + final HttpServletRequest request = createMock(HttpServletRequest.class); + PageBook pageBook = createMock(PageBook.class); + PageBook.Page page = createMock(PageBook.Page.class); + Renderable widget = createMock(Renderable.class); + final Respond respond = createMock(Respond.class); + RequestBinder binder = createMock(RequestBinder.class); + + Object pageOb = new Object(); + + expect(request.getRequestURI()) + .andReturn("/thing") + .anyTimes(); + + expect(request.getContextPath()) + .andReturn("") + .anyTimes(); + + expect(request.getParameterMap()) + .andReturn(new HashMap()) + .anyTimes(); + + expect(pageBook.get("/thing")) + .andReturn(page); + + binder.bind(request, pageOb); + expectLastCall().once(); + + + expect(page.isHeadless()) + .andReturn(false); + + expect(page.widget()) + .andReturn(widget); + + expect(page.instantiate()) + .andReturn(pageOb); + + expect(request.getMethod()) + .andReturn("POST"); + + //noinspection unchecked + expect(page.doMethod(matches("post"), eq(pageOb), eq("/thing"), isA(HttpServletRequest.class))) + .andReturn(null); +// expectLastCall().once(); + + + widget.render(pageOb, respond); + expectLastCall().once(); + + + replay(request, page, pageBook, widget, respond, binder); + + Respond out = new WidgetRoutingDispatcher(pageBook, binder, new Provider() { + public Respond get() { + return respond; + } + }, createNiceMock(ResourcesService.class), flashCacheProvider, + createNiceMock(HeadlessRenderer.class)).dispatch(request, response); + + + assert out == respond : "Did not respond correctly"; + + verify(request, page, pageBook, widget, respond, binder); + + } + + @Test + public final void dispatchRequestAndRedirectOnPost() throws IOException { + final HttpServletRequest request = createMock(HttpServletRequest.class); + PageBook pageBook = createMock(PageBook.class); + PageBook.Page page = createMock(PageBook.Page.class); + Renderable widget = createMock(Renderable.class); + final Respond respond = createMock(Respond.class); + RequestBinder binder = createMock(RequestBinder.class); + + Object pageOb = new Object(); + + expect(request.getRequestURI()) + .andReturn("/thing") + .anyTimes(); + + expect(request.getContextPath()) + .andReturn("") + .anyTimes(); + + expect(request.getParameterMap()) + .andReturn(new HashMap()) + .anyTimes(); + + expect(pageBook.get("/thing")) + .andReturn(page); + + binder.bind(request, pageOb); + expectLastCall().once(); + + expect(page.isHeadless()) + .andReturn(false); + + expect(page.instantiate()) + .andReturn(pageOb); + + expect(request.getMethod()) + .andReturn("POST"); + + respond.redirect(REDIRECTED_POST); + + //noinspection unchecked + expect(page.doMethod(matches("post"), eq(pageOb), eq("/thing"), isA(HttpServletRequest.class))) + .andReturn(REDIRECTED_POST); + + +// widget.render(pageOb, respond); +// expectLastCall().once(); + + + replay(request, page, pageBook, widget, respond, binder); + + Respond out = new WidgetRoutingDispatcher(pageBook, binder, new Provider() { + public Respond get() { + return respond; + } + }, createNiceMock(ResourcesService.class), flashCacheProvider, + createNiceMock(HeadlessRenderer.class)).dispatch(request, response); + + + assert out == respond : "Did not respond correctly"; + + verify(request, page, pageBook, widget, respond, binder); + + } + + @Test + public final void dispatchRequestAndRedirectOnGet() throws IOException { + final HttpServletRequest request = createMock(HttpServletRequest.class); + PageBook pageBook = createMock(PageBook.class); + PageBook.Page page = createMock(PageBook.Page.class); + Renderable widget = createMock(Renderable.class); + final Respond respond = createMock(Respond.class); + RequestBinder binder = createMock(RequestBinder.class); + + Object pageOb = new Object(); + + + expect(request.getRequestURI()) + .andReturn("/thing") + .anyTimes(); + + expect(request.getContextPath()) + .andReturn("") + .anyTimes(); + + expect(request.getParameterMap()) + .andReturn(new HashMap()) + .anyTimes(); + + expect(pageBook.get("/thing")) + .andReturn(page); + + binder.bind(request, pageOb); + expectLastCall().once(); + + expect(page.isHeadless()) + .andReturn(false); + + expect(page.instantiate()) + .andReturn(pageOb); + + expect(request.getMethod()) + .andReturn("GET"); + + respond.redirect(REDIRECTED_GET); + + //noinspection unchecked + expect(page.doMethod(matches("get"), eq(pageOb), eq("/thing"), isA(HttpServletRequest.class))) + .andReturn(REDIRECTED_GET); + + +// widget.render(pageOb, respond); +// expectLastCall().once(); + + + replay(request, page, pageBook, widget, respond, binder); + + Respond out = new WidgetRoutingDispatcher(pageBook, binder, new Provider() { + public Respond get() { + return respond; + } + }, createNiceMock(ResourcesService.class), flashCacheProvider, + createNiceMock(HeadlessRenderer.class)).dispatch(request, response); + + + assert out == respond : "Did not respond correctly"; + + verify(request, page, pageBook, widget, respond, binder); + + } + + @Test + public final void dispatchNothingBecauseOfNoUriMatch() throws IOException { + final HttpServletRequest request = createMock(HttpServletRequest.class); + PageBook pageBook = createMock(PageBook.class); + RequestBinder binder = createMock(RequestBinder.class); + + @SuppressWarnings("unchecked") + Provider respond = createMock(Provider.class); + + expect(request.getRequestURI()) + .andReturn(A_STATIC_RESOURCE_URI) + .anyTimes(); + + expect(request.getContextPath()) + .andReturn("") + .anyTimes(); + + expect(pageBook.get(A_STATIC_RESOURCE_URI)) + .andReturn(null); + + replay(request, pageBook, respond, binder); + + Respond out = new WidgetRoutingDispatcher(pageBook, binder, respond, createNiceMock(ResourcesService.class), + flashCacheProvider, + createNiceMock(HeadlessRenderer.class)).dispatch(request, response); + + + assert out == null : "Did not respond correctly"; + + verify(request, pageBook, respond, binder); + + } + + @Test + public final void dispatchStaticResource() throws IOException { + final HttpServletRequest request = createMock(HttpServletRequest.class); + PageBook pageBook = createMock(PageBook.class); + RequestBinder binder = createMock(RequestBinder.class); + ResourcesService resourcesService = createMock(ResourcesService.class); + Respond mockRespond = createMock(Respond.class); + + @SuppressWarnings("unchecked") + Provider respond = createMock(Provider.class); + + + expect(request.getRequestURI()) + .andReturn(A_STATIC_RESOURCE_URI) + .anyTimes(); + + expect(request.getContextPath()) + .andReturn("") + .anyTimes(); + + expect(resourcesService.serve(A_STATIC_RESOURCE_URI)) + .andReturn(mockRespond); + + replay(request, pageBook, respond, binder, resourcesService); + + Respond out = new WidgetRoutingDispatcher(pageBook, binder, respond, resourcesService, + flashCacheProvider, createNiceMock(HeadlessRenderer.class)).dispatch(request, response); + + + assert out != null : "Did not respond correctly"; + assert mockRespond.equals(out); + + verify(request, pageBook, respond, binder, resourcesService); + + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/test/ContentNegotiationExample.java b/sitebricks/src/test/java/com/google/sitebricks/test/ContentNegotiationExample.java new file mode 100644 index 00000000..aae73250 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/test/ContentNegotiationExample.java @@ -0,0 +1,28 @@ +package com.google.sitebricks.test; + +import com.google.sitebricks.At; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Select; +import com.google.sitebricks.Show; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@At("/aPage") @Show("Wiki.html") @Select("Accept") +public class ContentNegotiationExample { + private int counter; + + @Get("text/plain") @Show("text.txt") //Does not work yet!!! + public void textPage() { + + } + + @Get("text/html") @Show("text.html") //Does not work yet!!! + public void htmlPage() { + + } + + public int getCounter() { + return counter; + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/test/Search.html b/sitebricks/src/test/java/com/google/sitebricks/test/Search.html new file mode 100644 index 00000000..4d325cba --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/test/Search.html @@ -0,0 +1,50 @@ + + + + + + Warp :: Counter demo + + + + + +

+ This page demonstrates the use of \@\Managed properties in Warp with a counter. +
+
+ Counter value: ${counter} +
+ @TextField(counter) + + + + A repetition over property expression "$ {movieName}" + + @ShowIf(visible) + + @Repeat(items=movies, var="movie", pageVar="page") +

${movie.movieName}, released on + + @ShowIf(movie.name.matches("Under Siege")) + movie.releasedOn +

+ +

+ +
+ increment +
+
+ + + + + + + reset ctr +
+ reset ctr + + \ No newline at end of file diff --git a/sitebricks/src/test/java/com/google/sitebricks/test/Search.java b/sitebricks/src/test/java/com/google/sitebricks/test/Search.java new file mode 100644 index 00000000..6d2b2cd4 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/test/Search.java @@ -0,0 +1,49 @@ +package com.google.sitebricks.test; + +import com.google.sitebricks.At; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Select; + +import java.util.Collection; + +/** + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +@At("/wiki/search") @Select("event") +public class Search { //defaults to @Show("Search.xhtml"), or @Show("Search.html") + + private int counter; + private String query; //"get" param + private Collection movies; + + public Collection getMovies() { + return movies; + } + + + public static class Movie { + public String getMovieName() { + return "thing"; + } + + } + + @Get("results") + public void showResults() { //called after parameters are bound + } + + + //how about a search bar widget? + @Get("widget") + public void showSearchWidget() { + //don't need to do anything but you could set up some contextual info on the widget here + } + + public int getCounter() { + return counter; + } + + public String getQuery() { + return query; + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/test/Wiki.html b/sitebricks/src/test/java/com/google/sitebricks/test/Wiki.html new file mode 100644 index 00000000..1a09ab0a --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/test/Wiki.html @@ -0,0 +1,32 @@ + + + + + + Warp :: Counter demo + + + + +

+ This page demonstrates the use of @Managed properties in Warp with a counter. +
+
+ Counter value: ${counter} +
+ + + +

+ +
+ increment +
+ @ShowIf(true) + reset ctr +
+ + reset ctr + + \ No newline at end of file diff --git a/sitebricks/src/test/java/com/google/sitebricks/test/Wiki.java b/sitebricks/src/test/java/com/google/sitebricks/test/Wiki.java new file mode 100644 index 00000000..fdb68b5c --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/test/Wiki.java @@ -0,0 +1,35 @@ +package com.google.sitebricks.test; + +import com.google.inject.name.Named; +import com.google.sitebricks.At; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.Show; +import com.google.sitebricks.rendering.EmbedAs; +import com.google.sitebricks.rendering.resource.Assets; +import com.google.sitebricks.Export; + +/** + * + */ +@At("/wiki/page/:title") +@Show("Wiki.html") +@EmbedAs("Wiki") +@Assets({@Export(at = "/your.js", resource = "your.js")}) +public class Wiki { + private String title; + private String language; //"get" variable, bound by request parameter of same name, via setter + private String text; //"post" variable, bound similarly + private int counter; + + @Get + public void showPage(@Named("title") String title) { //URI-part extraction +// this.title = wikiFind.fetch(title); + //etc. + + //page is now rendered with the default view + } + + public int getCounter() { + return counter; + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/util/TextToolsTest.java b/sitebricks/src/test/java/com/google/sitebricks/util/TextToolsTest.java new file mode 100644 index 00000000..49cdb492 --- /dev/null +++ b/sitebricks/src/test/java/com/google/sitebricks/util/TextToolsTest.java @@ -0,0 +1,56 @@ +package com.google.sitebricks.util; + +import com.google.inject.Guice; +import com.google.inject.Module; +import com.google.sitebricks.compiler.*; +import com.google.sitebricks.rendering.DynTypedMvelEvaluatorCompiler; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.util.List; +import java.util.HashMap; + +/** + * Created with IntelliJ IDEA. + * On: Mar 25, 2007 12:06:10 PM + * + * @author Dhanji R. Prasanna (dhanji at gmail com) + */ +public class TextToolsTest { + private static final String TOKENS = "tokens"; + + + @DataProvider(name = TOKENS) + public final Object[][] tokens() { + return new Object[][]{ + {new String[]{"hello expr", "${expr}"}}, + {new String[]{"hello expr", "${expr}", "as $asd $ {}"}}, + {new String[]{"$$ { {}", "${}"}}, + }; + } + + @Test(dataProvider = TOKENS) + public final void testTokenize(String[] rawStream) throws ExpressionCompileException { + StringBuilder builder = new StringBuilder(); + for (String chunk : rawStream) + builder.append(chunk); + + List tokens = Parsing.tokenize(builder.toString(), + new DynTypedMvelEvaluatorCompiler(new HashMap>())); + + assertEquals(tokens.size(),rawStream.length); + + for (int i = 0; i < rawStream.length; i++) { + Token token = tokens.get(i); +// assert rawStream[i].equals(token.getToken()); + + if (rawStream[i].startsWith("${") && rawStream[i].endsWith("}")) + assertTrue(token.isExpression()); + else + assertTrue(!token.isExpression()); + } + } + +} diff --git a/sitebricks/src/test/resources/com/google/sitebricks/My.xml b/sitebricks/src/test/resources/com/google/sitebricks/My.xml new file mode 100644 index 00000000..57cda0a7 --- /dev/null +++ b/sitebricks/src/test/resources/com/google/sitebricks/My.xml @@ -0,0 +1,6 @@ + +

hello

+ + @ShowIf(true) +

Some text

+
\ No newline at end of file diff --git a/sitebricks/src/test/resources/com/google/sitebricks/MyHtml.html b/sitebricks/src/test/resources/com/google/sitebricks/MyHtml.html new file mode 100644 index 00000000..57cda0a7 --- /dev/null +++ b/sitebricks/src/test/resources/com/google/sitebricks/MyHtml.html @@ -0,0 +1,6 @@ + +

hello

+ + @ShowIf(true) +

Some text

+
\ No newline at end of file diff --git a/sitebricks/src/test/resources/com/google/sitebricks/MyXhtml.xhtml b/sitebricks/src/test/resources/com/google/sitebricks/MyXhtml.xhtml new file mode 100644 index 00000000..57cda0a7 --- /dev/null +++ b/sitebricks/src/test/resources/com/google/sitebricks/MyXhtml.xhtml @@ -0,0 +1,6 @@ + +

hello

+ + @ShowIf(true) +

Some text

+
\ No newline at end of file diff --git a/sitebricks/src/test/resources/com/google/sitebricks/rendering/resource/my.xml b/sitebricks/src/test/resources/com/google/sitebricks/rendering/resource/my.xml new file mode 100644 index 00000000..3ebc892e --- /dev/null +++ b/sitebricks/src/test/resources/com/google/sitebricks/rendering/resource/my.xml @@ -0,0 +1,3 @@ + + + diff --git a/slf4j/pom.xml b/slf4j/pom.xml new file mode 100644 index 00000000..bb1b837e --- /dev/null +++ b/slf4j/pom.xml @@ -0,0 +1,58 @@ + + 4.0.0 + + com.google.sitebricks + sitebricks-parent + 0.8.5 + + sitebricks-slf4j + Sitebricks :: SLF4J Module + Slf4j logging support for Guice applications (does not require sitebricks) + + + + org.testng + testng + ${org.testng.version} + jdk15 + test + + + com.google.inject + guice + + + ch.qos.logback + logback-classic + ${ch.qos.logback.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + + + + google-snapshots + Sonatype OSS Nexus Snapshots + https://oss.sonatype.org/content/repositories/google-snapshots + + + google-with-staging + Nexus OSS Staging Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + diff --git a/slf4j/src/main/java/com/google/sitebricks/slf4j/Slf4jInjectionTypeListener.java b/slf4j/src/main/java/com/google/sitebricks/slf4j/Slf4jInjectionTypeListener.java new file mode 100644 index 00000000..3e041ccb --- /dev/null +++ b/slf4j/src/main/java/com/google/sitebricks/slf4j/Slf4jInjectionTypeListener.java @@ -0,0 +1,55 @@ +package com.google.sitebricks.slf4j; + +import com.google.inject.ProvisionException; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.InjectionListener; +import com.google.inject.spi.TypeEncounter; +import com.google.inject.spi.TypeListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class Slf4jInjectionTypeListener implements TypeListener { + @Override + public void hear(final TypeLiteral type, TypeEncounter encounter) { + final Field field = getLoggerField(type.getRawType()); + + if (field != null) { + encounter.register(new InjectionListener() { + @Override + public void afterInjection(I injectee) { + try { + field.set(injectee, + LoggerFactory.getLogger(type.getRawType())); + } catch (IllegalAccessException e) { + throw new ProvisionException( + "Unable to inject SLF4J logger", e); + } + } + }); + } + } + + protected Field getLoggerField(Class clazz) { + // search for Logger in current class and return it if found + for (final Field field : clazz.getDeclaredFields()) { + final Class typeOfField = field.getType(); + if (Logger.class.isAssignableFrom(typeOfField)) { + + return field; + } + } + + // search for Logger in superclass if not found in this class + if (clazz.getSuperclass() != null) { + return getLoggerField(clazz.getSuperclass()); + } + + // not in current class and not having superclass, return null + return null; + } +} diff --git a/slf4j/src/main/java/com/google/sitebricks/slf4j/Slf4jModule.java b/slf4j/src/main/java/com/google/sitebricks/slf4j/Slf4jModule.java new file mode 100644 index 00000000..e029e2cb --- /dev/null +++ b/slf4j/src/main/java/com/google/sitebricks/slf4j/Slf4jModule.java @@ -0,0 +1,18 @@ +package com.google.sitebricks.slf4j; + +import com.google.inject.AbstractModule; + +import static com.google.inject.matcher.Matchers.any; + +/** + * Module to install which enables automatic injection of slf4j loggers into + * Guice-managed objects (by field injection only). + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class Slf4jModule extends AbstractModule { + @Override + protected void configure() { + bindListener(any(), new Slf4jInjectionTypeListener()); + } +} diff --git a/slf4j/src/test/java/com/google/sitebricks/slf4j/Slf4jIntegrationTest.java b/slf4j/src/test/java/com/google/sitebricks/slf4j/Slf4jIntegrationTest.java new file mode 100644 index 00000000..502bb70b --- /dev/null +++ b/slf4j/src/test/java/com/google/sitebricks/slf4j/Slf4jIntegrationTest.java @@ -0,0 +1,55 @@ +package com.google.sitebricks.slf4j; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.slf4j.Logger; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.AssertJUnit.assertNotNull; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class Slf4jIntegrationTest { + @Test + public final void testLoggerInjection() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new Slf4jModule()); + } + }); + + // simple case + AService instance = injector.getInstance(AService.class); + + assertNotNull(instance.log); + assertEquals(AService.class.getName(), instance.log.getName()); + + // Expect no exception thrown. + instance.log.debug("Works!"); + + // inherited logger + TheService inheritedInstance = injector.getInstance(TheService.class); + + assertNotNull(inheritedInstance.log); + assertEquals(TheService.class.getName(), + inheritedInstance.log.getName()); + + // Expect no exception thrown. + inheritedInstance.log.debug("Works!"); + } + + public static class AbstractService { + Logger log; + } + + public static class TheService extends AbstractService { + } + + public static class AService { + Logger log; + } +} diff --git a/stat/pom.xml b/stat/pom.xml new file mode 100644 index 00000000..492c926a --- /dev/null +++ b/stat/pom.xml @@ -0,0 +1,89 @@ + + 4.0.0 + + com.google.sitebricks + sitebricks-parent + 0.8.5 + + sitebricks-stat + Sitebricks :: Statistics + Statistics/Monitoring for Guice applications (does not require sitebricks) + + + + gson + http://google-gson.googlecode.com/svn/mavenrepo + + true + + + true + + + + + + + org.testng + testng + ${org.testng.version} + jdk15 + test + + + com.google.inject.extensions + guice-servlet + + + com.google.inject.extensions + guice-multibindings + + + com.google.code.gson + gson + + + com.google.guava + guava + + + com.google.inject + guice + + + commons-io + commons-io + + + javax.servlet + servlet-api + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + + + + google-snapshots + Sonatype OSS Nexus Snapshots + https://oss.sonatype.org/content/repositories/google-snapshots + + + google-with-staging + Nexus OSS Staging Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + diff --git a/stat/src/main/java/com/google/sitebricks/stat/MemberAnnotatedWithAtStat.java b/stat/src/main/java/com/google/sitebricks/stat/MemberAnnotatedWithAtStat.java new file mode 100644 index 00000000..2e194b3a --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/MemberAnnotatedWithAtStat.java @@ -0,0 +1,69 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + +import com.google.common.base.Objects; + +import java.lang.reflect.Member; + +/** + * This is a value object that contains information about a member + * annotated with {@link Stat}. + * + * @author ffaber@gmail.com (Fred Faber) + */ +final class MemberAnnotatedWithAtStat { + final Stat stat; + final Member member; + + MemberAnnotatedWithAtStat(Stat stat, Member member) { + this.stat = stat; + this.member = member; + } + + Stat getStat() { + return stat; + } + + @SuppressWarnings("unchecked") + T getMember() { + return (T) member; + } + + @Override public boolean equals(Object other) { + if (!(other instanceof MemberAnnotatedWithAtStat)) { + return false; + } + + MemberAnnotatedWithAtStat otherAnnotatedMember = + (MemberAnnotatedWithAtStat) other; + return Objects.equal(this.member, otherAnnotatedMember.member) + && Objects.equal(this.stat, otherAnnotatedMember.stat); + } + + @Override public int hashCode() { + return Objects.hashCode(stat, member); + } + + @Override public String toString() { + return Objects.toStringHelper(this) + .add("Stat", stat) + .add("Member", member) + .toString(); + } +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/Stat.java b/stat/src/main/java/com/google/sitebricks/stat/Stat.java new file mode 100644 index 00000000..a2d4c156 --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/Stat.java @@ -0,0 +1,28 @@ +package com.google.sitebricks.stat; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotate a variable to mark it as a statistic to be tracked. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface Stat { + + /** The name of the stat to track. */ + String value(); + + /** An optional human-readable description of this stat. */ + String description() default ""; + + /** + * Class of the exposer to apply before exposing a reference to the stat + * value. + */ + Class exposer() default StatExposers.InferenceExposer.class; +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatAnnotatedTypeListener.java b/stat/src/main/java/com/google/sitebricks/stat/StatAnnotatedTypeListener.java new file mode 100644 index 00000000..0fc62666 --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatAnnotatedTypeListener.java @@ -0,0 +1,32 @@ +package com.google.sitebricks.stat; + +import com.google.inject.TypeLiteral; +import com.google.inject.spi.InjectionListener; +import com.google.inject.spi.TypeEncounter; +import com.google.inject.spi.TypeListener; + +/** + * This listener registers an {@link InjectionListener} for each type + * for which it is notified. When its + * {@link InjectionListener#afterInjection(Object)} is invoked, a listener + * registers all annotated fields on the given injectee. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class StatAnnotatedTypeListener implements TypeListener { + + private final StatRegistrar statRegistrar; + + StatAnnotatedTypeListener(StatRegistrar statRegistrar) { + this.statRegistrar = statRegistrar; + } + + @Override + public void hear(TypeLiteral type, TypeEncounter encounter) { + encounter.register(new InjectionListener() { + @Override public void afterInjection(I injectee) { + statRegistrar.registerAllStatsOn(injectee); + } + }); + } +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatCollector.java b/stat/src/main/java/com/google/sitebricks/stat/StatCollector.java new file mode 100644 index 00000000..3e69c7bc --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatCollector.java @@ -0,0 +1,90 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; + +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; + +/** + * A {@link StatCollector} performs the work of scanning the members of a class + * to collect each member annotated with {@link Stat}. + * + * @author ffaber@gmail.com (Fred Faber) + */ +class StatCollector + implements Function, List> { + + enum StaticMemberPolicy { + INCLUDE_STATIC_MEMBERS { + @Override boolean shouldAccept(Member member) { + return isStaticMember(member); + } + }, + EXCLUDE_STATIC_MEMBERS { + @Override boolean shouldAccept(Member member) { + return !isStaticMember(member); + } + }; + + abstract boolean shouldAccept(Member member); + + private static boolean isStaticMember(Member member) { + return (member.getModifiers() & Modifier.STATIC) != 0; + } + } + + private final StaticMemberPolicy staticMemberPolicy; + + StatCollector(StaticMemberPolicy staticMemberPolicy) { + this.staticMemberPolicy = staticMemberPolicy; + } + + /** + * {@inheritDoc} + * + *

Climbs the class hierarchy finding all annotated members. + */ + @Override public List apply(Class clazz) { + List annotatedMembers = Lists.newArrayList(); + + for (Class currentClass = clazz; + currentClass != Object.class; + currentClass = currentClass.getSuperclass()) { + for (Method method : currentClass.getDeclaredMethods()) { + Stat stat = method.getAnnotation(Stat.class); + if (stat != null && staticMemberPolicy.shouldAccept(method)) { + annotatedMembers.add(new MemberAnnotatedWithAtStat(stat, method)); + } + } + for (Field field : currentClass.getDeclaredFields()) { + Stat stat = field.getAnnotation(Stat.class); + if (stat != null && staticMemberPolicy.shouldAccept(field)) { + annotatedMembers.add(new MemberAnnotatedWithAtStat(stat, field)); + } + } + } + + return annotatedMembers; + } +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatDescriptor.java b/stat/src/main/java/com/google/sitebricks/stat/StatDescriptor.java new file mode 100644 index 00000000..f76d7dc7 --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatDescriptor.java @@ -0,0 +1,77 @@ +package com.google.sitebricks.stat; + +import com.google.common.base.Objects; + +/** + * A {@link StatDescriptor} encapsulates the information required to publish + * a stat. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public final class StatDescriptor { + private final String name; + private final String description; + private final StatReader statReader; + private final Class statExposerClass; + + private StatDescriptor( + String name, String description, StatReader statReader, + Class statExposerClass) { + this.name = name; + this.description = description; + this.statReader = statReader; + this.statExposerClass = statExposerClass; + } + + static StatDescriptor of( + String name, String description, StatReader statReader, + Class statExposerClass) { + return new StatDescriptor(name, description, statReader, statExposerClass); + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public StatReader getStatReader() { + return statReader; + } + + public Class getStatExposerClass() { + return statExposerClass; + } + + @Override public String toString() { + return Objects.toStringHelper(this) + .add("name", name) + .add("description", description) + .add("statReader", statReader) + .add("statExposerClass", statExposerClass) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof StatDescriptor)) { + return false; + } + + StatDescriptor that = (StatDescriptor) o; + return Objects.equal(this.name, that.name) + && Objects.equal(this.description, that.description) + && Objects.equal(this.statReader, that.statReader) + && Objects.equal(this.statExposerClass, that.statExposerClass); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, description, statReader, statExposerClass); + } +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatExposer.java b/stat/src/main/java/com/google/sitebricks/stat/StatExposer.java new file mode 100644 index 00000000..03444ca4 --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatExposer.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + + +/** + * A {@link StatExposer} is responsible for exposing the value of a stat to + * the stat publishing logic. This is a layer of transformation that exists + * so that registered stats may be protected from leaking as mutable references + * from within the class in which they exist to the stat publishing logic. + * + * @author ffaber@gmail.com (Fred Faber) + */ +public interface StatExposer { + + /** Accepts the raw value of a stat, optionally transforms it, and returns + * an exposed value of the stat. This exposed value is what is passed to + * the stat publishing logic. + */ + Object expose(T target); +} \ No newline at end of file diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatExposers.java b/stat/src/main/java/com/google/sitebricks/stat/StatExposers.java new file mode 100644 index 00000000..c5aae1fa --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatExposers.java @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This class contains fundamental implementations of {@link StatExposer}, and + * offers static methods to obtain instances thereof. + * + * @author ffaber@gmail.com (Fred Faber) + */ +public final class StatExposers { + private StatExposers() { } + + /** + * This exposure performs some basic inferences on a value to determine + * how it should expose the value as an equivalent. As a default case, it + * returns the string value of the stat. + */ + public final static class InferenceExposer implements StatExposer { + @SuppressWarnings("unchecked") + @Override public Object expose(Object target) { + if (target instanceof List) { + return Collections.unmodifiableList((List) target); + } + if (target instanceof Map) { + return Collections.unmodifiableMap((Map) target); + } + if (target instanceof Set) { + return Collections.unmodifiableSet((Set) target); + } + return String.valueOf(target); + } + } + + /** This exposer returns the string value of a stat as its exposed form. */ + public final static class ToStringExposer implements StatExposer { + @Override public Object expose(Object target) { + return String.valueOf(target); + } + } + + /** This exposer returns the raw stat it is passed as its exposed form. */ + public final static class IdentityExposer implements StatExposer { + @Override public Object expose(Object target) { + return target; + } + } +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatModule.java b/stat/src/main/java/com/google/sitebricks/stat/StatModule.java new file mode 100644 index 00000000..2ff5e08f --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatModule.java @@ -0,0 +1,195 @@ +package com.google.sitebricks.stat; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.sitebricks.stat.StatsServlet.DEFAULT_FORMAT; + +import com.google.inject.matcher.Matchers; +import com.google.inject.multibindings.MapBinder; +import com.google.inject.servlet.ServletModule; +import com.google.sitebricks.stat.StatsPublishers.HtmlStatsPublisher; +import com.google.sitebricks.stat.StatsPublishers.JsonStatsPublisher; +import com.google.sitebricks.stat.StatsPublishers.TextStatsPublisher; + +/** + * This module enables publishing values annotated with {@link Stat} to a given + * servlet path. + *

+ *

Example of Use

+ * As an example, consider the following class: + *

+ * class QueryServlet extends HttpServlet {
+ *    {@literal @}Stat("search-hits")
+ *     private final AtomicInteger hits = new AtomicInteger(0);
+ *
+ *    {@literal @}Inject QueryServlet(....) { }
+ *
+ *    {@literal @}Override void doGet(
+ *        HttpServletRequest req, HttpServletResponse resp) {
+ *      ....
+ *      String searchTerm = req.getParameter("q");
+ *      SearchResult result = searchService.searchFor(searchTerm);
+ *      if (result.hasHits()) {
+ *        hits.incrementAndGet();
+ *      }
+ *      ...
+ *    }
+ * }
+ * 
+ *

+ * This class registers a stat called {@code search-hits}. To configure the + * server to publish this stat, install a {@link StatModule}, such as: + *


+ * public class YourServerModule extends AbstractModule {
+ *  {@literal @}Override protected void configure() {
+ *    install(new StatsModule("/stats");
+ *    ...
+ *   }
+ * }
+ * 
+ *

+ * Then, to query the server for its stats, hit the url that was registered + * with the module (which was {@code /stats}, in the example above). + * + *

Registering Stats

+ * The simplest way of registering a stat is to use the @Stat + * annotation. Members of a class annotated by @Stat are + * registered automatically when an instance of the class is created by Guice. + * The value of the member is read when a snapshot of the stats is requested, + * most likely by the {@link StatsServlet} upon a request to {@code /stats}. + *

+ * At times it is convenient to "manually" register a stat. To do this, + * inject an instance of {@link StatRegistrar} and use it to register a stat. + * For example: + *


+ * class RegistersLocalVariableAsStat {
+ *
+ *   private final StatRegistrar statRegistrar;
+ *
+ *   {@literal @}Inject RegistersLocalVariableAsStat(
+ *       StatRegistrar statRegistrar) {
+ *     this.statRegistrar = statRegistrar;
+ *    }
+ *
+ *   void initialize() {
+ *     long start = System.currentTimeMillis();
+ *     doInitialization();
+ *     statRegistrar.registerSingleStat(
+ *       "init-time-in-ms",
+ *       "Initialization time of a class",
+ *       System.currentTimeMillis() - start);
+ *   }
+ * }
+ * 
+ * There are other convenience methods on {@link StatRegistrar} to facilitate + * registering annotated static members on classes and registering all + * annotated members on instances as well. + * + *

Exposing Stats

+ * It's important to consider, if only to be careful, how to prevent a mutable + * reference of a stat from leaking into the stat publishing logic. For + * instance, if you were to publish a deeply mutable reference to a + * List, then a stat publisher could inadvertently (or purposely) + * mutate it. + *

+ * It is the role of a {@link StatExposer} to guard against such leaks: An + * exposer is given the raw value of a stat, and should return a safe view of + * it. This view is then passed to the {@link StatsPublishers publishers}. + *

+ * By default, a {@link StatExposers.InferenceExposer} is used to guard stats + * registered via {@link Stat @Stat}. This implementation should + * handle the majority of common use cases. If, however, you want to use a + * different {@link StatExposer} for your stat, then you may do so by + * specifying its class within the {@link Stat @Stat} annotation. + * For example: + *


+ * class ServiceStat implements Cloneable {
+ *   int calls;
+ *   AtomicLong<Long> latencyInMs;
+ *
+ *  {@literal @}Override protected Object clone() {
+ *     return new ServiceStat(calls, latencyInMs);
+ *   }
+ * }
+ *
+ * class ServiceStatExposer implements StatExposer<ServiceStat> {
+ *  {@literal @}Override Object expose(ServiceStat serviceStat) {
+ *     return serviceStat.clone();
+ *   }
+ * }
+ *
+ * class Service {
+ *  {@literal @}Stat(value = "service-stat", exposer = ServiceStatExposer.class)
+ *   private final ServiceStat serviceStat;
+ *
+ *   ...
+ * }
+ * 
+ * + *

Published Formats

+ * By default, published stats are available in several formats: + *
    + *
  • html - a formatted html page + *
  • json - well formed json + *
  • text - simple plaintext page + *
+ * To request stats in a given format, include a value for the + * {@value StatsServlet#DEFAULT_FORMAT} parameter in the {@code /stats} request. + * For the formats above, the value for this parameter should correspond to the + * type of output (i.e., pass "html", "json", or "text"). If no parameter is + * given, then html is returned. + *

+ *

Extensions

+ * You may extend the default set of publishers by adding and binding another + * implementation of {@link StatsPublisher}. To add your implementation, + * add a binding to a {@link MapBinder MapBinder<String, StatsPublisher>}. + * For example: + *

+ * public class CustomPublisherModule extends AbstractModule {
+ *   {@literal @}Override protected void configure() {
+ *      MapBinder<String, StatsPublisher> mapBinder =
+ *         MapBinder.newMapBinder(binder(), String.class, StatsPublisher.class);
+ *      mapBinder.addBinding("custom").to(CustomStatsPublisher.class);
+ *   }
+ * }
+ * 
+ * You can then retrieve stats from your custom publisher by hitting + * {@code /stats?format=custom}. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + * @author ffaber@gmail.com (Fred Faber) + */ +public class StatModule extends ServletModule { + private final String uriPath; + + public StatModule(String uriPath) { + checkArgument(!isNullOrEmpty(uriPath), + "URI path must be a valid non-empty servlet path mapping (example: /stats)"); + this.uriPath = uriPath; + } + + @Override + protected void configureServlets() { + // Manual bootstrapping is needed to instantiated a well-formed listener. + Stats stats = new Stats(); + bind(Stats.class).toInstance(stats); + requestInjection(stats); + + StatRegistrar statRegistrar = new StatRegistrar(stats); + bind(StatRegistrar.class).toInstance(statRegistrar); + + StatAnnotatedTypeListener listener = + new StatAnnotatedTypeListener(statRegistrar); + + bindListener(Matchers.any(), listener); + + serve(uriPath).with(StatsServlet.class); + + MapBinder publisherBinder = + MapBinder.newMapBinder(binder(), String.class, StatsPublisher.class); + publisherBinder.addBinding(DEFAULT_FORMAT).to(HtmlStatsPublisher.class); + publisherBinder.addBinding("html").to(HtmlStatsPublisher.class); + publisherBinder.addBinding("json").to(JsonStatsPublisher.class); + publisherBinder.addBinding("text").to(TextStatsPublisher.class); + } +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatReader.java b/stat/src/main/java/com/google/sitebricks/stat/StatReader.java new file mode 100644 index 00000000..b6c103c2 --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatReader.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + +/** + * A {@link StatReader} is ble to read the value of a stat, be it a direct + * reference to an object, a field or method on a class, or by some other + * means. + *

+ * To create an instance of this class, use the factory methods defined + * on {@link StatReaders}. + * + * @author ffaber@gmail.com (Fred Faber) + */ +public abstract class StatReader { + StatReader() { } + + /** Returns the value of the stat. */ + public abstract Object readStat(); + + abstract boolean equalsOtherStatReader(StatReader otherStatReader); + + abstract int hashCodeForStatReader(); + + @Override public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof StatReader)) { + return false; + } + + return equalsOtherStatReader((StatReader) o); + } + + @Override public int hashCode() { + return hashCodeForStatReader(); + } +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatReaders.java b/stat/src/main/java/com/google/sitebricks/stat/StatReaders.java new file mode 100644 index 00000000..f89e460e --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatReaders.java @@ -0,0 +1,189 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Objects; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; + +/** + * This class offers methods to obtain instances of {@link StatReader}. + * + * @author ffaber@gmail.com (Fred Faber) + */ +public final class StatReaders { + private StatReaders() { } + + public static StatReader forField(final Field field, final Object target) { + return new FieldBasedStatReader(field, target); + } + + public static StatReader forStaticField(Field field) { + return forField(field, null); + } + + public static StatReader forMethod( + final Method method, final Object target) { + return new MethodBasedStatReader(method, target); + } + + public static StatReader forStaticMethod(Method method) { + return forMethod(method, null); + } + + public static StatReader forStaticMember(Member member) { + return forMember(member, null); + } + + public static StatReader forMember(Member member, Object target) { + if (member instanceof Field) { + return forField((Field) member, target); + } + if (member instanceof Method) { + return forMethod((Method) member, target); + } + throw new IllegalArgumentException("Unsupported type of member: " + member); + } + + public static StatReader forObject(Object object) { + return new ObjectBasedStatReader(object); + } + + private static class FieldBasedStatReader extends StatReader { + private final Field field; + private final Object target; + + FieldBasedStatReader(Field field, Object target) { + this.field = field; + this.target = target; + } + + @Override public Object readStat() { + if (!field.isAccessible()) { + field.setAccessible(true); + } + try { + return field.get(target); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override boolean equalsOtherStatReader(StatReader otherStatReader) { + if (!FieldBasedStatReader.class.isInstance(otherStatReader)) { + return false; + } + FieldBasedStatReader that = + (FieldBasedStatReader) otherStatReader; + return Objects.equal(this.field, that.field) + && Objects.equal(this.target, that.target); + } + + @Override int hashCodeForStatReader() { + return Objects.hashCode(field, target); + } + + @Override public String toString() { + return Objects.toStringHelper(this) + .add("field", field) + .add("target", target) + .toString(); + } + } + + private static class MethodBasedStatReader extends StatReader { + private final Method method; + private final Object target; + + MethodBasedStatReader(Method method, Object target) { + this.method = method; + this.target = target; + } + + @Override public Object readStat() { + if (!method.isAccessible()) { + method.setAccessible(true); + } + try { + return method.invoke(target); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override boolean equalsOtherStatReader(StatReader otherStatReader) { + if (!MethodBasedStatReader.class.isInstance(otherStatReader)) { + return false; + } + MethodBasedStatReader that = + (MethodBasedStatReader) otherStatReader; + return Objects.equal(this.method, that.method) + && Objects.equal(this.target, that.target); + } + + @Override int hashCodeForStatReader() { + return Objects.hashCode(method, target); + } + + @Override public String toString() { + return Objects.toStringHelper(this) + .add("method", method) + .add("target", target) + .toString(); + } + } + + private static class ObjectBasedStatReader extends StatReader { + private final Object object; + + ObjectBasedStatReader(Object object) { + checkNotNull(object); + this.object = object; + } + + @Override public Object readStat() { + return object; + } + + @Override boolean equalsOtherStatReader(StatReader otherStatReader) { + if (!ObjectBasedStatReader.class.isInstance(otherStatReader)) { + return false; + } + ObjectBasedStatReader that = + (ObjectBasedStatReader) otherStatReader; + return this.object.equals(that.object); + } + + @Override int hashCodeForStatReader() { + return object.hashCode(); + } + + @Override public String toString() { + return Objects.toStringHelper(this) + .add("object", object) + .toString(); + } + } +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatRegistrar.java b/stat/src/main/java/com/google/sitebricks/stat/StatRegistrar.java new file mode 100644 index 00000000..4d69bcc7 --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatRegistrar.java @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + +import com.google.common.collect.MapMaker; + +import java.lang.reflect.Member; +import java.util.List; +import java.util.Map; + +/** + * A {@link StatRegistrar} offers the means by which to register a stat. + * + * @author ffaber@gmail.com (Fred Faber) + */ +public final class StatRegistrar { + /** + * The default exposer to use, which is suitable in most cases. + * Note that this would be best defined within {@link Stat}, but + * static fields declared within {@code @interface} definitions lead to + * javac bugs, such as is described here: + * https://bugs.eclipse.org/bugs/show_bug.cgi?id=324931 + */ + Class DEFAULT_EXPOSER_CLASS = StatExposers.InferenceExposer.class; + + private final Map, List> + classesToInstanceMembers = + new MapMaker().weakKeys().makeComputingMap( + new StatCollector( + StatCollector.StaticMemberPolicy.EXCLUDE_STATIC_MEMBERS)); + + private final Map, List> + classesToStaticMembers = + new MapMaker().weakKeys().makeComputingMap( + new StatCollector( + StatCollector.StaticMemberPolicy.INCLUDE_STATIC_MEMBERS)); + + private final Stats stats; + + StatRegistrar(Stats stats) { + this.stats = stats; + } + + public void registerSingleStat(String name, String description, Object stat) { + registerSingleStat( + name, description, StatReaders.forObject(stat), DEFAULT_EXPOSER_CLASS); + } + + public void registerSingleStat( + String name, String description, StatReader statReader, + Class statExposerClass) { + stats.register( + StatDescriptor.of(name, description, statReader, statExposerClass)); + } + + public void registerStaticStatsOn(Class clazz) { + List annotatedMembers = + classesToStaticMembers.get(clazz); + for (MemberAnnotatedWithAtStat annotatedMember : annotatedMembers) { + Stat stat = annotatedMember.getStat(); + stats.register(StatDescriptor.of( + stat.value(), + stat.description(), + StatReaders.forStaticMember(annotatedMember.getMember()), + stat.exposer())); + } + } + + public void registerAllStatsOn(Object target) { + List annotatedMembers = + classesToInstanceMembers.get(target.getClass()); + for (MemberAnnotatedWithAtStat annotatedMember : annotatedMembers) { + Stat stat = annotatedMember.getStat(); + stats.register(StatDescriptor.of( + stat.value(), + stat.description(), + StatReaders.forMember(annotatedMember.getMember(), target), + stat.exposer())); + } + registerStaticStatsOn(target.getClass()); + } +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/Stats.java b/stat/src/main/java/com/google/sitebricks/stat/Stats.java new file mode 100644 index 00000000..018099c1 --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/Stats.java @@ -0,0 +1,103 @@ +package com.google.sitebricks.stat; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.MapMaker; +import com.google.common.collect.Sets; +import com.google.inject.Inject; +import com.google.inject.Injector; + +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Logger; + +/** + * This class represents the collection of registered stats within an + * application. Its main roles are to act as a container for these stats, and + * to provide access to them through its {@link #snapshot()} method. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +final class Stats { + private static final Logger logger = + Logger.getLogger(Stats.class.getCanonicalName()); + + /** This is the value used for duplicate stats. */ + static final String DUPLICATED_STAT_VALUE = "duplicated value"; + + private final ConcurrentMap stats = + new MapMaker().makeMap(); + + private Injector injector; + + @Inject Stats() { } + + @SuppressWarnings("UnusedDeclaration") + @Inject void setInjector(Injector injector) { + this.injector = injector; + checkBindingsExistForExposers(); + } + + void register(StatDescriptor statDescriptor) { + String statName = statDescriptor.getName(); + StatDescriptor existingDescriptor = + stats.putIfAbsent(statName, statDescriptor); + + if (existingDescriptor != null && + !(existingDescriptor.getStatReader().equals( + statDescriptor.getStatReader()))) { + logger.warning(String.format( + "You have two non-static stats using the same name [%s], " + + "this is not allowed. \n" + + "First encounter: %s\nSecond encounter: %s", + statName, existingDescriptor, statDescriptor)); + + StatDescriptor syntheticDescriptor = StatDescriptor.of( + statName, "Placeholder for duplicate stat", + StatReaders.forObject(DUPLICATED_STAT_VALUE), + StatExposers.IdentityExposer.class); + stats.put(statName, syntheticDescriptor); + } else { + if (injector != null) { + injector.getBinding(statDescriptor.getStatExposerClass()); + } + stats.put(statName, statDescriptor); + } + } + + ImmutableMap snapshot() { + checkState(injector != null, + "Stats may not be snapshotted yet; injector has not been set"); + ImmutableMap.Builder builder = + ImmutableMap.builder(); + for (StatDescriptor statDescriptor : stats.values()) { + // Here we read the raw value + Object statValue = statDescriptor.getStatReader().readStat(); + + // And here we are careful to expose only the reference we should + StatExposer statExposer = + getStatExposer(statDescriptor.getStatExposerClass()); + @SuppressWarnings("unchecked") // We know we don't guarantee a here. + Object exposedValue = statExposer.expose(statValue); + builder.put(statDescriptor, exposedValue); + } + return builder.build(); + } + + private StatExposer getStatExposer( + Class statExposerClass) { + return injector.getInstance(statExposerClass); + } + + private void checkBindingsExistForExposers() { + // We do an up-front check of + Set> statExposerClasses = Sets.newHashSet(); + for (StatDescriptor statDescriptor : stats.values()) { + statExposerClasses.add(statDescriptor.getStatExposerClass()); + } + for (Class statExposerClass : statExposerClasses) { + injector.getBinding(statExposerClass); + } + } +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatsPublisher.java b/stat/src/main/java/com/google/sitebricks/stat/StatsPublisher.java new file mode 100644 index 00000000..322c02eb --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatsPublisher.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + +import com.google.common.collect.ImmutableMap; + +import java.io.PrintWriter; + +/** + * An implementation of a {@link StatsPublisher} is able to publish a snapshot + * of values, as annotated by {@link Stat}. + * + * @author ffaber@gmail.com (Fred Faber) + */ +public abstract class StatsPublisher { + /** + * Returns a string indicating the content type. The string should be a + * suitable value for setting the content type of a servlet request. + */ + protected abstract String getContentType(); + + /** + * Publishes the given {@code snapshot} to the given {@link PrintWriter}, + * where the values of the snapshot are the values of the stats by which + * each is keyed. + */ + protected abstract void publish( + ImmutableMap snapshot, PrintWriter writer); +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatsPublishers.java b/stat/src/main/java/com/google/sitebricks/stat/StatsPublishers.java new file mode 100644 index 00000000..919e3e86 --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatsPublishers.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.gson.Gson; + +import java.io.PrintWriter; +import java.util.Map; +import java.util.Map.Entry; + +/** + * This class contains a collection of {@link StatsPublisher} implementations. + * + * @author ffaber@gmail.com (Fred Faber) + */ +final class StatsPublishers { + private StatsPublishers() { } + + /** This {@link StatsPublisher} publishes snapshots as html. */ + static class HtmlStatsPublisher extends StatsPublisher { + + @Override protected String getContentType() { + return "text/html"; + } + + @Override protected void publish( + ImmutableMap snapshot, PrintWriter writer) { + writer.println(""); + for (Entry entry : snapshot.entrySet()) { + StatDescriptor statDescriptor = entry.getKey(); + writer.print(""); + writer.print(statDescriptor.getName()); + writer.print(": "); + writer.print(entry.getValue()); + writer.println("
"); + } + writer.println(""); + } + } + + /** This {@link StatsPublisher} publishes a snapshot as JSON. */ + static class JsonStatsPublisher extends StatsPublisher { + + @Override protected String getContentType() { + return "application/json"; + } + + @Override protected void publish( + ImmutableMap snapshot, PrintWriter writer) { + Gson gson = new Gson(); + Map valuesByName = Maps.newLinkedHashMap(); + for (Entry entry : snapshot.entrySet()) { + valuesByName.put(entry.getKey().getName(), entry.getValue()); + } + + String gsonString = gson.toJson(valuesByName); + writer.write(gsonString); + } + } + + /** This {@link StatsPublisher} publishes snapshots as text. */ + static class TextStatsPublisher extends StatsPublisher { + + @Override protected String getContentType() { + return "text/plain"; + } + + @Override protected void publish( + ImmutableMap snapshot, PrintWriter writer) { + for (Map.Entry entry : snapshot.entrySet()) { + writer.println(entry.getKey().getName() + " " + entry.getValue()); + } + } + } +} diff --git a/stat/src/main/java/com/google/sitebricks/stat/StatsServlet.java b/stat/src/main/java/com/google/sitebricks/stat/StatsServlet.java new file mode 100644 index 00000000..41cd74a9 --- /dev/null +++ b/stat/src/main/java/com/google/sitebricks/stat/StatsServlet.java @@ -0,0 +1,58 @@ +package com.google.sitebricks.stat; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Map; + +import static com.google.common.base.Objects.firstNonNull; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Throwables.getStackTraceAsString; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@Singleton +class StatsServlet extends HttpServlet { + static final String FORMAT_PARAM = "format"; + static final String DEFAULT_FORMAT = "default"; + + private final Stats stats; + private final Map publishersByFormat; + + @Inject + StatsServlet(Map publishersByFormat, Stats stats) { + this.publishersByFormat = publishersByFormat; + this.stats = stats; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setStatus(HttpServletResponse.SC_OK); + String format = firstNonNull(req.getParameter(FORMAT_PARAM), DEFAULT_FORMAT); + StatsPublisher publisher = checkNotNull(publishersByFormat.get(format), + "No publisher for format %s found in %s", + format, publishersByFormat); + + resp.setContentType(publisher.getContentType()); + + PrintWriter writer = resp.getWriter(); + try { + publisher.publish(stats.snapshot(), writer); + } catch (Exception e) { + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + writer.write(String.format( + "Exception publishing stats:\n%s", getStackTraceAsString(e))); + } finally { + writer.flush(); + writer.close(); + } + } +} diff --git a/stat/src/test/java/com/google/sitebricks/stat/StatExposersTest.java b/stat/src/test/java/com/google/sitebricks/stat/StatExposersTest.java new file mode 100644 index 00000000..1d5207ce --- /dev/null +++ b/stat/src/test/java/com/google/sitebricks/stat/StatExposersTest.java @@ -0,0 +1,114 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.testng.annotations.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.testng.Assert.assertEquals; +import static org.testng.AssertJUnit.assertSame; +import static org.testng.FileAssert.fail; + + +/** + * This class includes tests for the various implementations of + * {@link StatExposer} defined within the {@link StatExposers} class. + * + */ +public final class StatExposersTest { + + @SuppressWarnings("unchecked") + @Test + public void testInferenceExposer() { + StatExposers.InferenceExposer inferenceExposer = new StatExposers.InferenceExposer(); + + List rawList = Lists.newArrayList(1, 2, 3); + List exposedList = (List) inferenceExposer.expose(rawList); + assertEquals(rawList, exposedList); + try { + exposedList.add(4); + fail("Should not be able to modify the exposed value"); + } catch (UnsupportedOperationException expected) { + } + try { + exposedList.clear(); + fail("Should not be able to modify the exposed value"); + } catch (UnsupportedOperationException expected) { + } + + Map rawMap = new HashMap() {{ + put("1", 1); + put("2", 2); + put("3", 3); + }}; + Map exposedMap = + (Map) inferenceExposer.expose(rawMap); + assertEquals(rawMap, exposedMap); + try { + exposedMap.put("4", 4); + fail("Should not be able to modify the exposed value"); + } catch (UnsupportedOperationException expected) { + } + try { + exposedMap.remove("3"); + fail("Should not be able to modify the exposed value"); + } catch (UnsupportedOperationException expected) { + } + + Set rawSet = Sets.newHashSet(1, 2, 3); + Set exposedSet = (Set) inferenceExposer.expose(rawSet); + assertEquals(rawSet, exposedSet); + try { + exposedSet.add(4); + fail("Should not be able to modify the exposed value"); + } catch (UnsupportedOperationException expected) { + } + try { + exposedSet.remove(3); + fail("Should not be able to modify the exposed value"); + } catch (UnsupportedOperationException expected) { + } + + AtomicInteger rawAtomicInteger = new AtomicInteger(4); + String exposedAtomicInteger = + (String) inferenceExposer.expose(rawAtomicInteger); + assertEquals(String.valueOf(rawAtomicInteger.get()), exposedAtomicInteger); + } + + @Test public void testToStringExposer() { + StatExposers.ToStringExposer toStringExposer = new StatExposers.ToStringExposer(); + List rawList = Lists.newArrayList(1, 2, 3); + String exposedList = (String) toStringExposer.expose(rawList); + assertEquals(rawList.toString(), exposedList); + } + + @SuppressWarnings("unchecked") + @Test public void testIdentityExposer() { + StatExposers.IdentityExposer identityExposer = new StatExposers.IdentityExposer(); + List rawList = Lists.newArrayList(1, 2, 3); + List exposedList = (List) identityExposer.expose(rawList); + assertSame(rawList, exposedList); + } +} diff --git a/stat/src/test/java/com/google/sitebricks/stat/StatReadersTest.java b/stat/src/test/java/com/google/sitebricks/stat/StatReadersTest.java new file mode 100644 index 00000000..415ea79c --- /dev/null +++ b/stat/src/test/java/com/google/sitebricks/stat/StatReadersTest.java @@ -0,0 +1,143 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + +import com.google.common.collect.Lists; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; + +import static org.testng.Assert.assertEquals; + + +/** + * This class includes tests for the logic within {@link StatReaders}. + * + * @author ffaber@gmail.com (Fred Faber) + */ +public class StatReadersTest { + + static class TestClass { + static Integer staticField; + + static Integer getStaticFieldValue() { + return staticField; + } + + Integer instanceField; + + Integer getInstanceFieldValue() { + return instanceField; + } + } + + static final Integer STATIC_FIELD_STARTING_VALUE = 1; + static final Integer INSTANCE_FIELD_STARTING_VALUE = 2; + + Field staticField; + Method staticMethod; + Field instanceField; + Method instanceMethod; + TestClass testClass; + + @BeforeMethod + public void setUp() throws Exception { + testClass = new TestClass(); + TestClass.staticField = STATIC_FIELD_STARTING_VALUE; + testClass.instanceField = INSTANCE_FIELD_STARTING_VALUE; + + staticField = TestClass.class.getDeclaredField("staticField"); + staticMethod = TestClass.class.getDeclaredMethod("getStaticFieldValue"); + instanceField = TestClass.class.getDeclaredField("instanceField"); + instanceMethod = TestClass.class.getDeclaredMethod("getInstanceFieldValue"); + } + + @Test + public void testInstanceFieldReader() { + StatReader statReader = StatReaders.forField(instanceField, testClass); + assertEquals(testClass.instanceField, statReader.readStat()); + + // Mutate the field and confirm that its updated value is read. + testClass.instanceField++; + assertEquals(testClass.instanceField, statReader.readStat()); + } + + @Test public void testStaticFieldReader() { + StatReader statReader = StatReaders.forStaticField(staticField); + assertEquals(TestClass.staticField, statReader.readStat()); + + // Mutate the field and confirm that its updated value is read. + TestClass.staticField++; + assertEquals(TestClass.staticField, statReader.readStat()); + } + + @Test public void testInstanceMethodReader() { + StatReader statReader = StatReaders.forMethod(instanceMethod, testClass); + assertEquals(testClass.getInstanceFieldValue(), statReader.readStat()); + + // Mutate the field and confirm that its updated value is read. + testClass.instanceField++; + assertEquals(testClass.getInstanceFieldValue(), statReader.readStat()); + } + + @Test public void testStaticMethodReader() { + StatReader statReader = StatReaders.forStaticMethod(staticMethod); + assertEquals(TestClass.getStaticFieldValue(), statReader.readStat()); + + // Mutate the field and confirm that its updated value is read. + TestClass.staticField++; + assertEquals(TestClass.getStaticFieldValue(), statReader.readStat()); + } + + @Test public void testInstanceMemberReader_forField() { + StatReader memberReader = StatReaders.forMember(instanceField, testClass); + StatReader fieldReader = StatReaders.forField(instanceField, testClass); + assertEquals(memberReader, fieldReader); + } + + @Test public void testInstanceMemberReader_forMethod() { + StatReader memberReader = StatReaders.forMember(instanceMethod, testClass); + StatReader methodReader = StatReaders.forMethod(instanceMethod, testClass); + assertEquals(memberReader, methodReader); + } + + @Test public void testStaticMemberReader_forField() { + StatReader memberReader = StatReaders.forStaticMember(staticField); + StatReader fieldReader = StatReaders.forStaticField(staticField); + assertEquals(memberReader, fieldReader); + } + + @Test public void testStaticMemberReader_forMember() { + StatReader memberReader = StatReaders.forStaticMember(staticMethod); + StatReader methodReader = StatReaders.forStaticMethod(staticMethod); + assertEquals(memberReader, methodReader ); + } + + @Test public void testObjectReader() { + List statList = Lists.newArrayList(1, 2); + StatReader statReader = StatReaders.forObject(statList); + assertEquals(statList, statReader.readStat()); + + // If we add a value, we expect it to be reflected in the stat that is read + statList.add(3); + assertEquals(statList, statReader.readStat()); + } +} diff --git a/stat/src/test/java/com/google/sitebricks/stat/StatsIntegrationTest.java b/stat/src/test/java/com/google/sitebricks/stat/StatsIntegrationTest.java new file mode 100644 index 00000000..379831c7 --- /dev/null +++ b/stat/src/test/java/com/google/sitebricks/stat/StatsIntegrationTest.java @@ -0,0 +1,234 @@ +package com.google.sitebricks.stat; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.sitebricks.stat.testservices.ChildDummyService; +import com.google.sitebricks.stat.testservices.DummyService; +import com.google.sitebricks.stat.testservices.StatExposerTestingService; +import com.google.sitebricks.stat.testservices.StaticDummyService; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.testng.Assert.assertEquals; + +/** + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class StatsIntegrationTest { + Injector injector; + + @BeforeMethod + public final void before() { + injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new StatModule("/stat")); + + bind(DummyService.class); + bind(ChildDummyService.class); + bind(StatExposerTestingService.class); + } + }); + } + + @Test + public final void testPublishingStatsInDummyService() { + DummyService service = injector.getInstance(DummyService.class); + + service.call(); + service.call(); + service.call(); + + Stats stats = injector.getInstance(Stats.class); + ImmutableMap snapshot = stats.snapshot(); + + assertEquals(snapshot.size(), 2); + + // Here we check the value of the field, NUMBER_OF_CALLS + StatDescriptor numberOfCallsDescriptor = + getByName(DummyService.NUMBER_OF_CALLS, snapshot); + String numberOfCallsValue = (String) snapshot.get(numberOfCallsDescriptor); + assertEquals(String.valueOf(service.getCalls()), numberOfCallsValue); + + // Here we check the value of the method, CALL_LATENCY_NS + StatDescriptor callLatencyNsDescriptor = + getByName(DummyService.CALL_LATENCY_NS, snapshot); + String callLatencyValue = (String) snapshot.get(callLatencyNsDescriptor); + assertEquals(service.getCallLatencyMs().toString(), callLatencyValue); + } + + /** + * This test illustrates how stats are published from parent classes when + * a child class is published. + */ + @Test public void testPublishingStatsInChildService() { + ChildDummyService service = injector.getInstance(ChildDummyService.class); + + service.call(); + service.call(); + + Stats stats = injector.getInstance(Stats.class); + ImmutableMap snapshot = stats.snapshot(); + + // We expect 1 stat from the child class and 2 from its parent + assertEquals(snapshot.size(), 3); + StatDescriptor numberOfChildCallsDescriptor = + getByName(ChildDummyService.NUMBER_OF_CHILD_CALLS, snapshot); + String numberOfChildCallsValue = + (String) snapshot.get(numberOfChildCallsDescriptor); + assertEquals( + String.valueOf(service.getChildCalls()), numberOfChildCallsValue); + + // Below we check the value of the stats on the parent class + StatDescriptor numberOfCallsDescriptor = + getByName(DummyService.NUMBER_OF_CALLS, snapshot); + String numberOfCallsValue = (String) snapshot.get(numberOfCallsDescriptor); + assertEquals(String.valueOf(service.getCalls()), numberOfCallsValue); + + StatDescriptor callLatencyNsDescriptor = + getByName(DummyService.CALL_LATENCY_NS, snapshot); + String callLatencyValue = (String) snapshot.get(callLatencyNsDescriptor); + assertEquals(service.getCallLatencyMs().toString(), callLatencyValue); + } + + @Test + public void testPublishingStatsAsStaticMember() { + StaticDummyService.reset(); + StaticDummyService service1 = injector.getInstance(StaticDummyService.class); + StaticDummyService service2 = injector.getInstance(StaticDummyService.class); + + service1.call(); + service2.call(); + + Stats stats = injector.getInstance(Stats.class); + ImmutableMap snapshot = stats.snapshot(); + assertEquals(snapshot.size(), 1); + StatDescriptor staticCallsDescriptor = + getByName(StaticDummyService.STATIC_CALLS, snapshot); + String numberOfStaticCallsValue = + (String) snapshot.get(staticCallsDescriptor); + assertEquals( + String.valueOf(StaticDummyService.getNumberOfStaticCalls()), + numberOfStaticCallsValue); + } + + @Test + public final void testPublishingDuplicatedStat() { + DummyService service1 = injector.getInstance(DummyService.class); + DummyService service2 = injector.getInstance(DummyService.class); + + service1.call(); + service2.call(); + + Stats stats = injector.getInstance(Stats.class); + ImmutableMap snapshot = stats.snapshot(); + + assertEquals(2, snapshot.size(), snapshot.toString()); + for (Entry entry : snapshot.entrySet()) { + assertEquals(Stats.DUPLICATED_STAT_VALUE, entry.getValue(), + "Unexpected value for " + entry.getKey()); + } + } + + @SuppressWarnings("unchecked") + @Test + public final void testPublishingUsingDifferentExposers() { + StatExposerTestingService service = + injector.getInstance(StatExposerTestingService.class); + + service.call(); + service.call(); + + Stats stats = injector.getInstance(Stats.class); + ImmutableMap snapshot = stats.snapshot(); + assertEquals(snapshot.size(), 8, "Snapshot has unexpected size: " + snapshot); + + AtomicInteger atomicIntegerCallCount = service.getCallCount(); + String stringCallCount = String.valueOf(atomicIntegerCallCount); + + StatDescriptor callsDefaultExposerDescriptor = getByName( + StatExposerTestingService.CALLS_WITH_DEFAULT_EXPOSER, snapshot); + String callsDefaultExposerValue = + (String) snapshot.get(callsDefaultExposerDescriptor); + assertEquals(stringCallCount, callsDefaultExposerValue); + + StatDescriptor callsIdentityExposerDescriptor = getByName( + StatExposerTestingService.CALLS_WITH_IDENTITY_EXPOSER, snapshot); + AtomicInteger callsIdentityExposerValue = + (AtomicInteger) snapshot.get(callsIdentityExposerDescriptor); + assertEquals(atomicIntegerCallCount.get(), callsIdentityExposerValue.get()); + + StatDescriptor callsInferenceExposerDescriptor = getByName( + StatExposerTestingService.CALLS_WITH_INFERENCE_EXPOSER, snapshot); + String callsInferenceExposerValue = + (String) snapshot.get(callsInferenceExposerDescriptor); + assertEquals(stringCallCount, callsInferenceExposerValue); + + StatDescriptor callsToStringExposerDescriptor = getByName( + StatExposerTestingService.CALLS_WITH_TO_STRING_EXPOSER, snapshot); + String callsToStringExposerValue = + (String) snapshot.get(callsToStringExposerDescriptor); + assertEquals(stringCallCount, callsToStringExposerValue); + + List callsList = service.getCallsList(); + String callsListAsString = String.valueOf(callsList); + + StatDescriptor listDefaultExposerDescriptor = getByName( + StatExposerTestingService.LIST_WITH_DEFAULT_EXPOSER, snapshot); + List listDefaultExposerValue = + (List) snapshot.get(listDefaultExposerDescriptor); + assertEquals(callsList, listDefaultExposerValue); + + StatDescriptor listIdentityExposerDescriptor = getByName( + StatExposerTestingService.LIST_WITH_IDENTITY_EXPOSER, snapshot); + List listIdentityExposerValue = + (List) snapshot.get(listIdentityExposerDescriptor); + assertEquals(callsList, listIdentityExposerValue); + + StatDescriptor listInferenceExposerDescriptor = getByName( + StatExposerTestingService.LIST_WITH_INFERENCE_EXPOSER, snapshot); + List listInferenceExposerValue = + (List) snapshot.get(listInferenceExposerDescriptor); + assertEquals(callsList, listInferenceExposerValue); + + StatDescriptor listToStringExposerDescriptor = getByName( + StatExposerTestingService.LIST_WITH_TO_STRING_EXPOSER, snapshot); + String listToStringExposerValue = + (String) snapshot.get(listToStringExposerDescriptor); + assertEquals(callsListAsString, listToStringExposerValue); + } + + @Test + public final void testPublishingStandaloneStat() { + StatRegistrar statRegistrar = injector.getInstance(StatRegistrar.class); + + AtomicInteger statValue = new AtomicInteger(0); + statRegistrar.registerSingleStat("single-stat", "", statValue); + + Stats stats = injector.getInstance(Stats.class); + ImmutableMap snapshot = stats.snapshot(); + + StatDescriptor numberOfChildCallsDescriptor = + getByName("single-stat", snapshot); + String snapshottedValue = + (String) snapshot.get(numberOfChildCallsDescriptor); + assertEquals(String.valueOf(statValue.intValue()), snapshottedValue); + } + + StatDescriptor getByName( + String name, ImmutableMap snapshot) { + for (StatDescriptor key : snapshot.keySet()) { + if (key.getName().equals(name)) { + return key; + } + } + throw new RuntimeException( + "No entry found for " + name + " within " + snapshot); + } +} diff --git a/stat/src/test/java/com/google/sitebricks/stat/StatsPublishersTest.java b/stat/src/test/java/com/google/sitebricks/stat/StatsPublishersTest.java new file mode 100644 index 00000000..ec2586f0 --- /dev/null +++ b/stat/src/test/java/com/google/sitebricks/stat/StatsPublishersTest.java @@ -0,0 +1,105 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.sitebricks.stat.StatsPublishers.HtmlStatsPublisher; +import com.google.sitebricks.stat.StatsPublishers.JsonStatsPublisher; +import com.google.sitebricks.stat.StatsPublishers.TextStatsPublisher; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import static org.testng.Assert.assertEquals; + + +/** + * This test class contains tests for the various types of + * {@link StatsPublisher publishers} that are defined within the + * {@code com.google.inject.stat} package. + * + * @author ffaber@gmail.com (Fred Faber) + */ +public class StatsPublishersTest { + private static final String NL = System.getProperty("line.separator"); + + StringWriter stringWriter; + PrintWriter printWriter; + ImmutableMap snapshot = + ImmutableMap.builder() + .put(StatDescriptor.of("int-stat", "", null, null), 3) + .put(StatDescriptor.of("float-stat", "", null, null), 4.3d) + .put(StatDescriptor.of("list-stat", "", null, null), + ImmutableList.of("a", "b", "c")) + .build(); + + @BeforeMethod + public final void before() { + stringWriter = new StringWriter(1024); + printWriter = new PrintWriter(stringWriter); + } + + @Test + public void testHtmlPublisher() { + HtmlStatsPublisher publisher = new HtmlStatsPublisher(); + String expectedOutput = new StringBuilder() + .append("").append(NL) + .append("int-stat: 3
").append(NL) + .append("float-stat: 4.3
").append(NL) + .append("list-stat: [a, b, c]
").append(NL) + .append("").append(NL) + .toString(); + assertPublishing(publisher, expectedOutput); + } + + @Test public void testJsonPublisher() { + JsonStatsPublisher publisher = new JsonStatsPublisher(); + String expectedOutput = new StringBuilder() + .append("{") + .append("\"int-stat\":3") + .append(",") + .append("\"float-stat\":4.3") + .append(",") + .append("\"list-stat\":[\"a\",\"b\",\"c\"]") + .append("}") + .toString(); + assertPublishing(publisher, expectedOutput); + } + + @Test public void testTextPublisher() { + TextStatsPublisher publisher = new TextStatsPublisher(); + String expectedOutput = new StringBuilder() + .append("int-stat 3").append(NL) + .append("float-stat 4.3").append(NL) + .append("list-stat [a, b, c]").append(NL) + .toString(); + assertPublishing(publisher, expectedOutput); + } + + private void assertPublishing( + StatsPublisher publisher, String expectedOutput) { + publisher.publish(snapshot, printWriter); + printWriter.flush(); + assertEquals(expectedOutput, stringWriter.getBuffer().toString()); + } +} diff --git a/stat/src/test/java/com/google/sitebricks/stat/testservices/ChildDummyService.java b/stat/src/test/java/com/google/sitebricks/stat/testservices/ChildDummyService.java new file mode 100644 index 00000000..f5dca75d --- /dev/null +++ b/stat/src/test/java/com/google/sitebricks/stat/testservices/ChildDummyService.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat.testservices; + +import com.google.sitebricks.stat.Stat; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This subclass of {@link DummyService} exists to illustrate how stats are + * published (or not published) within a class hierarchy. + * + * @author ffaber@gmail.com (Fred Faber) + */ +public class ChildDummyService extends DummyService { + + public static final String NUMBER_OF_CHILD_CALLS = "number-of-child-calls"; + + private final AtomicInteger childCalls = new AtomicInteger(); + + @Override public void call() { + super.call(); + childCalls.incrementAndGet(); + } + + @Stat(NUMBER_OF_CHILD_CALLS) + public AtomicInteger getChildCalls() { + return childCalls; + } +} diff --git a/stat/src/test/java/com/google/sitebricks/stat/testservices/DummyService.java b/stat/src/test/java/com/google/sitebricks/stat/testservices/DummyService.java new file mode 100644 index 00000000..792c36bc --- /dev/null +++ b/stat/src/test/java/com/google/sitebricks/stat/testservices/DummyService.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat.testservices; + +import com.google.sitebricks.stat.Stat; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This is a simple dummy class that is used in tests to ensure that a private + * field on a class outside of a package can have its {@link com.google.sitebricks.stat.Stat} fields read + * and published. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class DummyService { + public static final String NUMBER_OF_CALLS = "number-of-calls"; + + @Stat(NUMBER_OF_CALLS) + private final AtomicInteger calls = new AtomicInteger(); + + public static final String CALL_LATENCY_NS = "call-latency-ns"; + + private final AtomicLong callLatencyNs = new AtomicLong(0L); + + /** + * Increments {@link #calls} by one. Also increments {@link #callLatencyNs} + * by the amount of time required to do so.. + */ + public void call() { + Long startTime = System.nanoTime(); + try { + calls.incrementAndGet(); + } finally { + callLatencyNs.addAndGet(System.nanoTime() - startTime); + } + } + + public AtomicInteger getCalls() { + return calls; + } + + @Stat(CALL_LATENCY_NS) + public Long getCallLatencyMs() { + return callLatencyNs.get(); + } +} diff --git a/stat/src/test/java/com/google/sitebricks/stat/testservices/StatExposerTestingService.java b/stat/src/test/java/com/google/sitebricks/stat/testservices/StatExposerTestingService.java new file mode 100644 index 00000000..459dd5c2 --- /dev/null +++ b/stat/src/test/java/com/google/sitebricks/stat/testservices/StatExposerTestingService.java @@ -0,0 +1,98 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat.testservices; + +import com.google.common.collect.Lists; +import com.google.sitebricks.stat.Stat; +import com.google.sitebricks.stat.StatExposers; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class has the main purpose of containing {@link com.google.sitebricks.stat.Stat}-annotated members + * so that it may be used to test {@code StatExposer}-based functionality. + * + * @author ffaber@gmail.com (Fred Faber) + */ +public class StatExposerTestingService { + public static final String CALLS_WITH_DEFAULT_EXPOSER = + "calls-with-default-exposer"; + public static final String CALLS_WITH_IDENTITY_EXPOSER = + "calls-with-identity-exposer"; + public static final String CALLS_WITH_INFERENCE_EXPOSER = + "calls-with-inference-exposer"; + public static final String CALLS_WITH_TO_STRING_EXPOSER = + "calls-with-to-string-exposer"; + + @Stat(CALLS_WITH_DEFAULT_EXPOSER) + private final AtomicInteger callsWithDefaultExposer = new AtomicInteger(); + + @Stat(value = CALLS_WITH_IDENTITY_EXPOSER, exposer = StatExposers.IdentityExposer.class) + private final AtomicInteger callsWithIdentityExposer = new AtomicInteger(); + + @Stat(value = CALLS_WITH_INFERENCE_EXPOSER, exposer = StatExposers.InferenceExposer.class) + private final AtomicInteger callsWithInferenceExposer = new AtomicInteger(); + + @Stat(value = CALLS_WITH_TO_STRING_EXPOSER, exposer = StatExposers.ToStringExposer.class) + private final AtomicInteger callsWithToStringExposer = new AtomicInteger(); + + public static final String LIST_WITH_DEFAULT_EXPOSER = + "list-with-default-exposer"; + public static final String LIST_WITH_IDENTITY_EXPOSER = + "list-with-identity-exposer"; + public static final String LIST_WITH_INFERENCE_EXPOSER = + "list-with-inference-exposer"; + public static final String LIST_WITH_TO_STRING_EXPOSER = + "list-with-to-string-exposer"; + + @Stat(LIST_WITH_DEFAULT_EXPOSER) + private final List listWithDefaultExposer = Lists.newArrayList(); + + @Stat(value = LIST_WITH_IDENTITY_EXPOSER, exposer = StatExposers.IdentityExposer.class) + private final List listWithIdentityExposer = Lists.newArrayList(); + + @Stat(value = LIST_WITH_INFERENCE_EXPOSER, exposer = StatExposers.InferenceExposer.class) + private final List listWithInferenceExposer = Lists.newArrayList(); + + @Stat(value = LIST_WITH_TO_STRING_EXPOSER , exposer = StatExposers.ToStringExposer.class) + private final List listWithToStringExposer = Lists.newArrayList(); + + /** Increments all counters by one, and adds an element to each list */ + public void call() { + callsWithDefaultExposer.incrementAndGet(); + callsWithIdentityExposer.incrementAndGet(); + callsWithInferenceExposer.incrementAndGet(); + callsWithToStringExposer.incrementAndGet(); + + listWithDefaultExposer.add(callsWithDefaultExposer.get()); + listWithIdentityExposer.add(callsWithIdentityExposer.get()); + listWithInferenceExposer.add(callsWithInferenceExposer.get()); + listWithToStringExposer.add(callsWithToStringExposer.get()); + } + + /** Returns the number of invocations of {@link #call()} */ + public AtomicInteger getCallCount() { + return new AtomicInteger(callsWithDefaultExposer.get()); + } + + /** Returns the value of each of the lists on this instance. */ + public List getCallsList() { + return Lists.newArrayList(listWithDefaultExposer); + } +} diff --git a/stat/src/test/java/com/google/sitebricks/stat/testservices/StaticDummyService.java b/stat/src/test/java/com/google/sitebricks/stat/testservices/StaticDummyService.java new file mode 100644 index 00000000..b4124cf5 --- /dev/null +++ b/stat/src/test/java/com/google/sitebricks/stat/testservices/StaticDummyService.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.google.sitebricks.stat.testservices; + +import com.google.sitebricks.stat.Stat; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class is a simple test-based service that has static members that are + * tested for {@link com.google.sitebricks.stat.Stat}-based publishing. + * + * @author ffaber@gmail.com (Fred Faber) + */ +public class StaticDummyService { + public static final String STATIC_CALLS = "static-calls"; + + @Stat(STATIC_CALLS) + private static final AtomicInteger calls = new AtomicInteger(); + + public static int getNumberOfStaticCalls() { + return calls.intValue(); + } + + public void call() { + calls.incrementAndGet(); + } + + public static void reset() { + calls.set(0); + } +} From 087cbf76eb341845ac864b4221629151cb935209 Mon Sep 17 00:00:00 2001 From: james Date: Mon, 12 Dec 2011 09:42:04 -0600 Subject: [PATCH 03/20] 0.8.5 sitebricks from github --- .gitignore | 22 ++++ LICENSE | 202 ++++++++++++++++++++++++++++++++++++ NOTICE | 10 ++ README.md | 36 +++++++ pom.xml | 297 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 567 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b35da380 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Maven +target/ +*.ser +*.ec + +# IntelliJ Idea +.idea/ +out/ +*.ipr +*.iws +*.iml + +# Eclipse +.classpath +.project +.settings/ + +# Svn +.svn/ + +# Other +bin/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..a5ad4e3b --- /dev/null +++ b/NOTICE @@ -0,0 +1,10 @@ + Sitebricks + Copyright 2007-11 Dhanji R. Prasanna + + This product includes software developed at + Google, Inc. (http://google.com/). + + Portions of this product were developed by individual + contributors who hold full rights to their work under + the Apache Software License 2.0. See class file headers + for specifics. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..60a044ba --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +Sitebricks +---------- + +Sitebricks is a simple set of libraries for web applications. Sitebricks focuses on early error + detection, low-footprint code, and fast development. Powered by + [Guice](http://code.google.com/p/google-guice), it also balances idiomatic + Java with an emphasis on concise code. + +### Early error detection ### + +This following misspelling results in a template compile error: + + + ${person.naem} + All such errors are picked up early and reported at once in a format similar to javac. + + 1) unknown or unresolvable property: naem + + 15: + 16: ${person.naem} + ^ + + Total errors: 1 + +### Next steps ### + +Get started +5-minute tutorial +Building RESTful web services + +* * * + +We would love your contributions and help in developing Sitebricks at this early stage. If you'd +like to contribute or have any questions, please join the [mailing list](http://groups.google.com/group/google-sitebricks). + +Or send us a note on twitter [@dhanji](http://twitter.com/dhanji) =) \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..10e2cc95 --- /dev/null +++ b/pom.xml @@ -0,0 +1,297 @@ + + 4.0.0 + + org.sonatype.oss + oss-parent + 6 + + com.google.sitebricks + sitebricks-parent + pom + 0.8.5 + Sitebricks :: Parent + https://github.com/dhanji/sitebricks + + + 3.0 + 1.8.0 + 6.1.9 + 0.9.7376 + 5.8 + 0.9.9 + + + + sitebricks + sitebricks-client + sitebricks-converter + sitebricks-acceptance-tests + + sitebricks-mail + sitebricks-options + sitebricks-jetty-archetype + stat + slf4j + + + + + + com.google.sitebricks + sitebricks-converter + ${project.version} + + + com.google.sitebricks + sitebricks + ${project.version} + + + com.google.sitebricks + stat + ${project.version} + + + com.google.sitebricks + sitebricks-client + ${project.version} + + + + org.mvel + mvel2 + 2.0.18 + + + + com.google.guava + guava + r07 + + + + com.google.code.gson + gson + 1.6 + + + + com.google.inject + guice + ${guice.version} + + + com.google.inject.extensions + guice-servlet + ${guice.version} + + + com.google.inject.extensions + guice-multibindings + ${guice.version} + + + + net.jcip + jcip-annotations + 1.0 + + + + com.intellij + annotations + 7.0.3 + + + + org.freemarker + freemarker + 2.3.10 + + + + commons-io + commons-io + 1.4 + + + + commons-collections + commons-collections + 20040616 + + + + commons-lang + commons-lang + 2.5 + + + + com.ning + async-http-client + 1.6.3 + + + + javax.servlet + servlet-api + 2.5 + + + + com.thoughtworks.xstream + xstream + 1.3.1 + + + + org.codehaus.jackson + jackson-core-asl + ${org.codehaus.jackson.version} + + + org.codehaus.jackson + jackson-mapper-asl + ${org.codehaus.jackson.version} + + + + jaxen + jaxen + 1.1.1 + + + + saxpath + saxpath + 1.0-FCS + + + + dom4j + dom4j + 1.6.1 + + + + + org.jsoup + jsoup + 1.4.1 + + + + + org.slf4j + slf4j-api + 1.5.5 + + + + ch.qos.logback + logback-classic + ${ch.qos.logback.version} + + + ch.qos.logback + logback-core + ${ch.qos.logback.version} + + + + org.easymock + easymock + 2.4 + test + + + + org.mortbay.jetty + jetty + ${org.mortbay.jetty.version} + + + org.mortbay.jetty + jetty-util + ${org.mortbay.jetty.version} + + + org.mortbay.jetty + servlet-api-2.5 + ${org.mortbay.jetty.version} + + + + org.seleniumhq.webdriver + webdriver-common + ${org.seleniumhq.webdriver.version} + test + + + org.seleniumhq.webdriver + webdriver-support + ${org.seleniumhq.webdriver.version} + test + + + org.seleniumhq.webdriver + webdriver-htmlunit + ${org.seleniumhq.webdriver.version} + test + + + org.testng + testng + ${org.testng.version} + jdk15 + test + + + + + + scm:git:git@github.com:dhanji/sitebricks.git + scm:git:git@github.com:dhanji/sitebricks.git + https://github.com/dhanji/sitebricks + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + + + + + + + + + google-snapshots + Sonatype OSS Nexus Snapshots + https://oss.sonatype.org/content/repositories/google-snapshots + + + google-with-staging + Nexus OSS Staging Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + scala.org + Scala.org Repository + http://scala-tools.org/repo-releases/ + + + From 01a0d7ba265d3361ccb16634368dd4c5e6d2e59f Mon Sep 17 00:00:00 2001 From: james Date: Mon, 12 Dec 2011 12:59:34 -0600 Subject: [PATCH 04/20] velocity template acceptance test passes --- .../sitebricks/example/SitebricksConfig.java | 217 +++++++------ .../sitebricks/example/VelocitySample.java | 21 ++ .../src/main/resources/VelocitySample.vm | 11 + .../VelocitySampleAcceptanceTest.java | 24 ++ .../acceptance/page/VelocitySamplePage.java | 33 ++ sitebricks/pom.xml | 5 + .../java/com/google/sitebricks/Template.java | 64 ++-- .../com/google/sitebricks/TemplateLoader.java | 284 +++++++++--------- .../google/sitebricks/compiler/Compilers.java | 47 ++- .../compiler/StandardCompilers.java | 258 ++++++++-------- .../template/VelocityTemplateCompiler.java | 54 ++++ .../src/main/resources/velocity.properties | 2 + 12 files changed, 594 insertions(+), 426 deletions(-) create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/VelocitySample.java create mode 100644 sitebricks-acceptance-tests/src/main/resources/VelocitySample.vm create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/VelocitySampleAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/VelocitySamplePage.java create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java create mode 100644 sitebricks/src/main/resources/velocity.properties diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java index fdb8d17b..61ba0b4d 100644 --- a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java @@ -1,5 +1,12 @@ package com.google.sitebricks.example; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + import com.google.common.collect.ImmutableMap; import com.google.inject.BindingAnnotation; import com.google.inject.Guice; @@ -7,7 +14,6 @@ import com.google.inject.Singleton; import com.google.inject.Stage; import com.google.inject.servlet.GuiceServletContextListener; -import com.google.sitebricks.stat.StatModule; import com.google.sitebricks.AwareModule; import com.google.sitebricks.SitebricksModule; import com.google.sitebricks.binding.FlashCache; @@ -18,128 +24,121 @@ import com.google.sitebricks.http.Get; import com.google.sitebricks.http.Post; import com.google.sitebricks.routing.Action; - -import javax.servlet.http.HttpServletRequest; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Locale; -import java.util.Map; +import com.google.sitebricks.stat.StatModule; /** * @author Dhanji R. Prasanna (dhanji@gmail.com) */ public class SitebricksConfig extends GuiceServletContextListener { - // a weird format - public static final String DEFAULT_DATE_TIME_FORMAT = "dd MM yy SS"; + // a weird format + public static final String DEFAULT_DATE_TIME_FORMAT = "dd MM yy SS"; + + @Override + protected Injector getInjector() { + return Guice.createInjector(Stage.DEVELOPMENT, new SitebricksModule() { + + @Override + protected void configureSitebricks() { + + // TODO(dhanji): find a way to run the suite again with this module installed. + // install(new GaeModule()); + + bind(FlashCache.class).to(HttpSessionFlashCache.class).in(Singleton.class); + + // TODO We should run the test suite once with this turned off and with scan() on. + // scan(SitebricksConfig.class.getPackage()); + bindExplicitly(); + bindActions(); + + at("/no_annotations/service").serve(RestfulWebServiceNoAnnotations.class); + at("/debug").show(DebugPage.class); + + bind(Start.class).annotatedWith(Test.class).to(Start.class); + + // Localize using the default translation set (i.e. from the @Message annotations) + localize(I18n.MyMessages.class).usingDefault(); + localize(I18n.MyMessages.class).using(Locale.CANADA_FRENCH, + ImmutableMap.of(I18n.HELLO, I18n.HELLO_IN_FRENCH)); + + install(new StatModule("/stats")); + + converter(new DateConverters.DateStringConverter(DEFAULT_DATE_TIME_FORMAT)); + + install(new AwareModule() { + @Override + protected void configureLifecycle() { + observe(StartAware.class).asEagerSingleton(); + } + }); + } + + private void bindExplicitly() { + //TODO explicit bindings should override scanned ones. + at("/").show(Start.class); + at("/hello").show(HelloWorld.class); + at("/case").show(Case.class); + at("/embed").show(Embed.class); + at("/error").show(CompileErrors.class); + at("/forms").show(Forms.class); + at("/repeat").show(Repeat.class); + at("/showif").show(ShowIf.class); + at("/dynamic.js").show(DynamicJs.class); + + at("/conversion").show(Conversion.class); -@Override - protected Injector getInjector() { - return Guice.createInjector(Stage.DEVELOPMENT, new SitebricksModule() { + at("/hiddenfieldmethod").show(HiddenFieldMethod.class); + at("/select").show(SelectRouting.class); + at("/conneg").show(ContentNegotiation.class); - @Override - protected void configureSitebricks() { + at("/helloservice").serve(HelloWorldService.class); - // TODO(dhanji): find a way to run the suite again with this module installed. -// install(new GaeModule()); + at("/service").serve(RestfulWebService.class); + at("/postable").serve(PostableRestfulWebService.class); + at("/superpath").serve(RestfulWebServiceWithSubpaths.class); + at("/matrixpath").serve(RestfulWebServiceWithMatrixParams.class); + at("/superpath2/:dynamic").serve(RestfulWebServiceWithSubpaths2.class); + at("/json/:type").serve(RestfulWebServiceWithCRUD.class); + at("/jsonConversion").serve(RestfulWebServiceWithCRUDConversions.class); - bind(FlashCache.class).to(HttpSessionFlashCache.class).in(Singleton.class); + at("/pagechain").show(PageChain.class); + at("/nextpage").show(NextPage.class); - // TODO We should run the test suite once with this turned off and with scan() on. -// scan(SitebricksConfig.class.getPackage()); - bindExplicitly(); - bindActions(); + at("/i18n").show(I18n.class); - at("/no_annotations/service").serve(RestfulWebServiceNoAnnotations.class); - at("/debug").show(DebugPage.class); + // MVEL template. + at("/template/mvel").show(MvelTemplateExample.class); - bind(Start.class).annotatedWith(Test.class).to(Start.class); + // templating by extension + at("/template").show(DecoratedPage.class); + at("/velocitySample").show(VelocitySample.class); - // Localize using the default translation set (i.e. from the @Message annotations) - localize(I18n.MyMessages.class).usingDefault(); - localize(I18n.MyMessages.class).using(Locale.CANADA_FRENCH, - ImmutableMap.of(I18n.HELLO, I18n.HELLO_IN_FRENCH)); - - install(new StatModule("/stats")); - - converter(new DateConverters.DateStringConverter(DEFAULT_DATE_TIME_FORMAT)); + embed(HelloWorld.class).as("Hello"); + } - install(new AwareModule() { - @Override - protected void configureLifecycle() { - observe(StartAware.class).asEagerSingleton(); - } + private void bindActions() { + at("/spi/test").perform(action("get:top")).on(Get.class).perform(action("post:junk_subpath1")) + .on(Post.class); + } }); - } - - private void bindExplicitly() { - //TODO explicit bindings should override scanned ones. - at("/").show(Start.class); - at("/hello").show(HelloWorld.class); - at("/case").show(Case.class); - at("/embed").show(Embed.class); - at("/error").show(CompileErrors.class); - at("/forms").show(Forms.class); - at("/repeat").show(Repeat.class); - at("/showif").show(ShowIf.class); - at("/dynamic.js").show(DynamicJs.class); - - at("/conversion").show(Conversion.class); - - at("/hiddenfieldmethod").show(HiddenFieldMethod.class); - at("/select").show(SelectRouting.class); - at("/conneg").show(ContentNegotiation.class); - - at("/helloservice").serve(HelloWorldService.class); - - at("/service").serve(RestfulWebService.class); - at("/postable").serve(PostableRestfulWebService.class); - at("/superpath").serve(RestfulWebServiceWithSubpaths.class); - at("/matrixpath").serve(RestfulWebServiceWithMatrixParams.class); - at("/superpath2/:dynamic").serve(RestfulWebServiceWithSubpaths2.class); - at("/json/:type").serve(RestfulWebServiceWithCRUD.class); - at("/jsonConversion").serve(RestfulWebServiceWithCRUDConversions.class); - - at("/pagechain").show(PageChain.class); - at("/nextpage").show(NextPage.class); - - at("/i18n").show(I18n.class); - - // MVEL template. - at("/template/mvel").show(MvelTemplateExample.class); - - // templating by extension - at("/template").show(DecoratedPage.class); - - embed(HelloWorld.class).as("Hello"); - } - - private void bindActions() { - at("/spi/test") - .perform(action("get:top")) - .on(Get.class) - .perform(action("post:junk_subpath1")) - .on(Post.class); - } - }); - } - - private Action action(final String reply) { - return new Action() { - @Override - public boolean shouldCall(HttpServletRequest request) { - return true; - } - - @Override - public Object call(Object page, Map map) { - return Reply.with(reply); - } - }; - } - - @BindingAnnotation - @Retention(RetentionPolicy.RUNTIME) - public static @interface Test { - } + } + + private Action action(final String reply) { + return new Action() { + @Override + public boolean shouldCall(HttpServletRequest request) { + return true; + } + + @Override + public Object call(Object page, Map map) { + return Reply.with(reply); + } + }; + } + + @BindingAnnotation + @Retention(RetentionPolicy.RUNTIME) + public static @interface Test { + } } diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/VelocitySample.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/VelocitySample.java new file mode 100644 index 00000000..00e64b9d --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/VelocitySample.java @@ -0,0 +1,21 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.http.Get; + +public class VelocitySample { + + public static final String MSG = "Yaba Daba Doo!"; + + @Get + public void get() { + System.out.println("velocity sample. woot!"); + } + + public String getName() { + return "fred flinstone"; + } + + public String getMessage() { + return MSG; + } +} diff --git a/sitebricks-acceptance-tests/src/main/resources/VelocitySample.vm b/sitebricks-acceptance-tests/src/main/resources/VelocitySample.vm new file mode 100644 index 00000000..aefe3361 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/VelocitySample.vm @@ -0,0 +1,11 @@ + + +velocity sample + + +

velocity sample

+

$page.name

+

$page.message

+ + + \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/VelocitySampleAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/VelocitySampleAcceptanceTest.java new file mode 100644 index 00000000..72be5382 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/VelocitySampleAcceptanceTest.java @@ -0,0 +1,24 @@ +package com.google.sitebricks.acceptance; + +import org.openqa.selenium.WebDriver; +import org.testng.annotations.Test; + +import com.google.sitebricks.acceptance.page.VelocitySamplePage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; + +@Test(suiteName = AcceptanceTest.SUITE) +public class VelocitySampleAcceptanceTest { + + public void shouldRenderDynamicTextFromVelocitySample() { + WebDriver driver = AcceptanceTest.createWebDriver(); + VelocitySamplePage page = VelocitySamplePage.open(driver, "/velocitySample"); + + assertVelocitySampleContent(page); + } + + private void assertVelocitySampleContent(VelocitySamplePage page) { + String title = page.getTitle(); + assert title.equals("velocity sample") : title + " != \"velocity sample\"\n" + page.getContent(); + assert page.hasMessage() : "did not have dynamic text"; + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/VelocitySamplePage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/VelocitySamplePage.java new file mode 100644 index 00000000..e5d394f2 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/VelocitySamplePage.java @@ -0,0 +1,33 @@ +package com.google.sitebricks.acceptance.page; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.PageFactory; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.example.VelocitySample; + +public class VelocitySamplePage { + private final WebDriver driver; + + public VelocitySamplePage(WebDriver driver) { + this.driver = driver; + } + + public static VelocitySamplePage open(WebDriver driver, String url) { + driver.get(AcceptanceTest.BASE_URL + url); + return PageFactory.initElements(driver, VelocitySamplePage.class); + } + + public String getTitle() { + return driver.getTitle(); + } + + public boolean hasMessage() { + System.out.println(driver.getPageSource()); + return driver.getPageSource().contains(VelocitySample.MSG); + } + + public String getContent() { + return driver.getPageSource(); + } +} diff --git a/sitebricks/pom.xml b/sitebricks/pom.xml index 9df036e3..e4875395 100644 --- a/sitebricks/pom.xml +++ b/sitebricks/pom.xml @@ -16,6 +16,11 @@ jdk15 test + + org.apache.velocity + velocity + 1.7 + com.google.sitebricks sitebricks-converter diff --git a/sitebricks/src/main/java/com/google/sitebricks/Template.java b/sitebricks/src/main/java/com/google/sitebricks/Template.java index 27bf3014..00183674 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/Template.java +++ b/sitebricks/src/main/java/com/google/sitebricks/Template.java @@ -4,41 +4,43 @@ * @author Dhanji R. Prasanna (dhanji@gmail com) */ public class Template { - private final Kind templateKind; - private final String text; + private final Kind templateKind; + private final String text; - public Template(Kind templateKind, String text) { - this.templateKind = templateKind; - this.text = text; - } + public Template(Kind templateKind, String text) { + this.templateKind = templateKind; + this.text = text; + } - public Kind getKind() { - return templateKind; - } + public Kind getKind() { + return templateKind; + } - public String getText() { - return text; - } + public String getText() { + return text; + } - public static enum Kind { - HTML, XML, FLAT, MVEL, FREEMARKER; + public static enum Kind { + HTML, XML, FLAT, MVEL, FREEMARKER, VELOCITY; - /** - * Returns whether a given template should be treated as html, xml or flat - * (currently by looking at file extension) - */ - public static Kind kindOf(String template) { - if (template.endsWith(".html") || template.endsWith(".xhtml")) - return HTML; - else if (template.endsWith(".xml")) - return XML; - else if (template.endsWith(".mvel")) - return MVEL; - else if (template.endsWith(".fml")) - return FREEMARKER; - else - return FLAT; - } + /** + * Returns whether a given template should be treated as html, xml or flat + * (currently by looking at file extension) + */ + public static Kind kindOf(String template) { + if (template.endsWith(".html") || template.endsWith(".xhtml")) + return HTML; + else if (template.endsWith(".xml")) + return XML; + else if (template.endsWith(".mvel")) + return MVEL; + else if (template.endsWith(".fml")) + return FREEMARKER; + else if (template.endsWith(".vm")) + return VELOCITY; + else + return FLAT; + } - } + } } diff --git a/sitebricks/src/main/java/com/google/sitebricks/TemplateLoader.java b/sitebricks/src/main/java/com/google/sitebricks/TemplateLoader.java index 2566d2d1..c9029192 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/TemplateLoader.java +++ b/sitebricks/src/main/java/com/google/sitebricks/TemplateLoader.java @@ -1,176 +1,184 @@ package com.google.sitebricks; -import com.google.inject.Inject; -import com.google.inject.Provider; -import net.jcip.annotations.Immutable; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; import javax.servlet.ServletContext; -import java.io.*; -import java.net.URL; + +import net.jcip.annotations.Immutable; + +import com.google.inject.Inject; +import com.google.inject.Provider; /** * @author Dhanji R. Prasanna (dhanji@gmail.com) */ @Immutable public class TemplateLoader { - private final Provider context; - - private final String[] fileNameTemplates = new String[] { "%s.html", "%s.xhtml", "%s.xml", - "%s.txt", "%s.fml", "%s.mvel" }; - - @Inject - public TemplateLoader(Provider context) { - this.context = context; - } - - public Template load(Class pageClass) { - // try to find the template name - Show show = pageClass.getAnnotation(Show.class); - String template = null; - if (null != show) { - template = show.value(); - } - - // an empty string means no template name was given - if (template == null || template.length() == 0) { - // use the default name for the page class - template = resolve(pageClass); + private final Provider context; + + private final String[] fileNameTemplates = new String[] { "%s.html", "%s.xhtml", "%s.xml", "%s.txt", "%s.fml", + "%s.mvel", "%s.vm" }; + + @Inject + public TemplateLoader(Provider context) { + this.context = context; } - String text; - try { - InputStream stream = null; - //first look in class neighborhood for template - if (null != template) { - stream = pageClass.getResourceAsStream(template); - } - - //look on the webapp resource path if not in classpath - if (null == stream) { - - final ServletContext servletContext = context.get(); - if (null != template) - stream = open(template, servletContext); - - //resolve again, but this time on the webapp resource path - if (null == stream) { - final ResolvedTemplate resolvedTemplate = resolve(pageClass, servletContext, template); - if (null != resolvedTemplate) { - template = resolvedTemplate.templateName; - stream = resolvedTemplate.resource; - } + public Template load(Class pageClass) { + // try to find the template name + Show show = pageClass.getAnnotation(Show.class); + String template = null; + if (null != show) { + template = show.value(); } - //if there's still no template, then error out - if (null == stream) { - throw new MissingTemplateException(String.format("Could not find a suitable template for %s, " + - "did you remember to place an @Show? None of [" + - fileNameTemplates[0] + - "] could be found in either package [%s], in the root of the resource dir OR in WEB-INF/.", - pageClass.getName(), pageClass.getSimpleName(), - pageClass.getPackage().getName())); + // an empty string means no template name was given + if (template == null || template.length() == 0) { + // use the default name for the page class + template = resolve(pageClass); } - } - text = read(stream); - } catch (IOException e) { - throw new TemplateLoadingException("Could not load template for (i/o error): " + pageClass, e); - } + String text; + try { + InputStream stream = null; + //first look in class neighborhood for template + if (null != template) { + stream = pageClass.getResourceAsStream(template); + } + + //look on the webapp resource path if not in classpath + if (null == stream) { + + final ServletContext servletContext = context.get(); + if (null != template) + stream = open(template, servletContext); + + //resolve again, but this time on the webapp resource path + if (null == stream) { + final ResolvedTemplate resolvedTemplate = resolve(pageClass, servletContext, template); + if (null != resolvedTemplate) { + template = resolvedTemplate.templateName; + stream = resolvedTemplate.resource; + } + } + + //if there's still no template, then error out + if (null == stream) { + throw new MissingTemplateException( + String.format( + "Could not find a suitable template for %s, " + + "did you remember to place an @Show? None of [" + + fileNameTemplates[0] + + "] could be found in either package [%s], in the root of the resource dir OR in WEB-INF/.", + pageClass.getName(), pageClass.getSimpleName(), pageClass.getPackage().getName())); + } + } + + text = read(stream); + } catch (IOException e) { + throw new TemplateLoadingException("Could not load template for (i/o error): " + pageClass, e); + } - return new Template(Template.Kind.kindOf(template), text); - } + return new Template(Template.Kind.kindOf(template), text); + } - private ResolvedTemplate resolve(Class pageClass, ServletContext context, String template) { - //first resolve using url conversion - for (String nameTemplate : fileNameTemplates) { - String templateName = String.format(nameTemplate, pageClass.getSimpleName()); - InputStream resource = open(templateName, context); + private ResolvedTemplate resolve(Class pageClass, ServletContext context, String template) { + //first resolve using url conversion + for (String nameTemplate : fileNameTemplates) { + String templateName = String.format(nameTemplate, pageClass.getSimpleName()); + InputStream resource = open(templateName, context); - if (null != resource) { - return new ResolvedTemplate(templateName, resource); - } + if (null != resource) { + return new ResolvedTemplate(templateName, resource); + } - resource = openWebInf(templateName, context); + resource = openWebInf(templateName, context); - if (null != resource) { - return new ResolvedTemplate(templateName, resource); - } + if (null != resource) { + return new ResolvedTemplate(templateName, resource); + } + if (null == template) { + continue; + } + //try to resolve @Show template from web-inf folder + resource = openWebInf(template, context); - if (null == template) { - continue; - } - //try to resolve @Show template from web-inf folder - resource = openWebInf(template, context); + if (null != resource) { + return new ResolvedTemplate(template, resource); + } + } - if (null != resource) { - return new ResolvedTemplate(template, resource); - } - } + //resolve again using servlet context if that fails + for (String nameTemplate : fileNameTemplates) { + String templateName = String.format(nameTemplate, pageClass.getSimpleName()); + InputStream resource = context.getResourceAsStream(templateName); - //resolve again using servlet context if that fails - for (String nameTemplate : fileNameTemplates) { - String templateName = String.format(nameTemplate, pageClass.getSimpleName()); - InputStream resource = context.getResourceAsStream(templateName); + if (null != resource) { + return new ResolvedTemplate(templateName, resource); + } + } - if (null != resource) { - return new ResolvedTemplate(templateName, resource); - } + return null; } - return null; - } - - private static class ResolvedTemplate { - private final InputStream resource; - private final String templateName; + private static class ResolvedTemplate { + private final InputStream resource; + private final String templateName; - private ResolvedTemplate(String templateName, InputStream resource) { - this.templateName = templateName; - this.resource = resource; + private ResolvedTemplate(String templateName, InputStream resource) { + this.templateName = templateName; + this.resource = resource; + } } - } - - private static InputStream open(String file, ServletContext context) { - try { - String path = context.getRealPath(file); - return path == null ? null : new FileInputStream(new File(path)); - } catch (FileNotFoundException e) { - return null; + + private static InputStream open(String file, ServletContext context) { + try { + String path = context.getRealPath(file); + return path == null ? null : new FileInputStream(new File(path)); + } catch (FileNotFoundException e) { + return null; + } } - } - - private static InputStream openWebInf(String file, ServletContext context) { - return open("/WEB-INF/" + file, context); - } - - //resolves a location for this page class's template (assuming @Show is not present) - private String resolve(Class pageClass) { - for (String nameTemplate : fileNameTemplates) { - String name = String.format(nameTemplate, pageClass.getSimpleName()); - URL resource = pageClass.getResource(name); - - if (null != resource) { - return name; - } + + private static InputStream openWebInf(String file, ServletContext context) { + return open("/WEB-INF/" + file, context); } - return null; - } + //resolves a location for this page class's template (assuming @Show is not present) + private String resolve(Class pageClass) { + for (String nameTemplate : fileNameTemplates) { + String name = String.format(nameTemplate, pageClass.getSimpleName()); + URL resource = pageClass.getResource(name); - private static String read(InputStream stream) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8")); + if (null != resource) { + return name; + } + } - StringBuilder builder = new StringBuilder(); - try { - while (reader.ready()) { - builder.append(reader.readLine()); - builder.append("\n"); - } - } finally { - stream.close(); + return null; } - return builder.toString(); - } + private static String read(InputStream stream) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8")); + + StringBuilder builder = new StringBuilder(); + try { + while (reader.ready()) { + builder.append(reader.readLine()); + builder.append("\n"); + } + } finally { + stream.close(); + } + + return builder.toString(); + } } diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/Compilers.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/Compilers.java index 055dd6e4..ee031447 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/compiler/Compilers.java +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/Compilers.java @@ -9,35 +9,34 @@ */ @ImplementedBy(StandardCompilers.class) public interface Compilers { - Renderable compileHtml(Class page, String template); + Renderable compileHtml(Class page, String template); - Renderable compileXml(Class page, String template); + Renderable compileXml(Class page, String template); - Renderable compileFlat(Class page, String template); + Renderable compileFlat(Class page, String template); - /** - * Creates a Renderable that can process MVEL templates. - * These are not to be confused with Sitebricks templates - * that *use* MVEL. Rather, this is MVEL's template technology. - */ - Renderable compileMvel(Class page, String template); + /** + * Creates a Renderable that can process MVEL templates. + * These are not to be confused with Sitebricks templates + * that *use* MVEL. Rather, this is MVEL's template technology. + */ + Renderable compileMvel(Class page, String template); - Renderable compileFreemarker( Class page, String text ); - - Renderable compileVelocity(Class page, String template); + Renderable compileFreemarker(Class page, String text); - /** - * Performs static analysis of the given page class to - * determine some types of errors. - */ - void analyze(Class page); - + Renderable compileVelocity(Class page, String template); - void compilePage(PageBook.Page page); + /** + * Performs static analysis of the given page class to + * determine some types of errors. + */ + void analyze(Class page); - /** - * Convenience method, use this instead of compileXXX to hide - * the underlying template type if you dont care what it is. - */ - Renderable compile(Class templateClass); + void compilePage(PageBook.Page page); + + /** + * Convenience method, use this instead of compileXXX to hide + * the underlying template type if you dont care what it is. + */ + Renderable compile(Class templateClass); } diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java index 087c53ea..a571a237 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java @@ -1,5 +1,9 @@ package com.google.sitebricks.compiler; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Map; + import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.sitebricks.Bricks; @@ -9,6 +13,7 @@ import com.google.sitebricks.Template; import com.google.sitebricks.TemplateLoader; import com.google.sitebricks.compiler.template.MvelTemplateCompiler; +import com.google.sitebricks.compiler.template.VelocityTemplateCompiler; import com.google.sitebricks.compiler.template.freemarker.FreemarkerTemplateCompiler; import com.google.sitebricks.headless.Reply; import com.google.sitebricks.rendering.Decorated; @@ -16,10 +21,6 @@ import com.google.sitebricks.routing.PageBook; import com.google.sitebricks.routing.SystemMetrics; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.Map; - /** * A factory for internal template compilers. * @@ -27,132 +28,141 @@ */ @Singleton class StandardCompilers implements Compilers { - private final WidgetRegistry registry; - private final PageBook pageBook; - private final SystemMetrics metrics; - private final Map> httpMethods; - private final TemplateLoader loader; - - @Inject - public StandardCompilers(WidgetRegistry registry, PageBook pageBook, SystemMetrics metrics, - @Bricks Map> httpMethods, TemplateLoader loader) { - this.registry = registry; - this.pageBook = pageBook; - this.metrics = metrics; - this.httpMethods = httpMethods; - this.loader = loader; - } - - public Renderable compileXml(Class page, String template) { - return new XmlTemplateCompiler(page, new MvelEvaluatorCompiler(page), registry, pageBook, - metrics) - .compile(template); - } - - public Renderable compileHtml(Class page, String template) { - return new HtmlTemplateCompiler(page, new MvelEvaluatorCompiler(page), registry, pageBook, - metrics) - .compile(template); - } - - public Renderable compileFlat(Class page, String template) { - return new FlatTemplateCompiler(page, new MvelEvaluatorCompiler(page), metrics, registry) - .compile(template); - } - - public Renderable compileMvel(Class page, String template) { - return new MvelTemplateCompiler(page).compile(template); - } - - public Renderable compileFreemarker( Class page, String template ) { - return new FreemarkerTemplateCompiler(page).compile(template); - } - - // TODO(dhanji): Feedback errors as return rather than throwing. - public void analyze(Class page) { - // May move this into a separate class if it starts getting too big. - analyzeMethods(page.getDeclaredMethods()); - analyzeMethods(page.getMethods()); - } - - private void analyzeMethods(Method[] methods) { - for (Method method : methods) { - for (Annotation annotation : method.getDeclaredAnnotations()) { - // if this is a http method annotation, do some checking on the - // args and return types. - if (httpMethods.containsValue(annotation.annotationType())) { - Class returnType = method.getReturnType(); - - PageBook.Page page = pageBook.forClass(returnType); - if (null == page) { - // throw an error. - } else { - // do further analysis on this sucka - if (page.getUri().contains(":")) - ; // throw an error coz we cant redir to dynamic URLs - - - // If this is headless, it MUST return an instance of reply. - if (page.isHeadless()) { - if (!Reply.class.isAssignableFrom(method.getReturnType())) { - // throw error - } + private final WidgetRegistry registry; + private final PageBook pageBook; + private final SystemMetrics metrics; + private final Map> httpMethods; + private final TemplateLoader loader; + + @Inject + public StandardCompilers(WidgetRegistry registry, PageBook pageBook, SystemMetrics metrics, + @Bricks Map> httpMethods, TemplateLoader loader) { + this.registry = registry; + this.pageBook = pageBook; + this.metrics = metrics; + this.httpMethods = httpMethods; + this.loader = loader; + } + + @Override + public Renderable compileXml(Class page, String template) { + return new XmlTemplateCompiler(page, new MvelEvaluatorCompiler(page), registry, pageBook, metrics) + .compile(template); + } + + @Override + public Renderable compileHtml(Class page, String template) { + return new HtmlTemplateCompiler(page, new MvelEvaluatorCompiler(page), registry, pageBook, metrics) + .compile(template); + } + + @Override + public Renderable compileFlat(Class page, String template) { + return new FlatTemplateCompiler(page, new MvelEvaluatorCompiler(page), metrics, registry).compile(template); + } + + @Override + public Renderable compileMvel(Class page, String template) { + return new MvelTemplateCompiler(page).compile(template); + } + + @Override + public Renderable compileFreemarker(Class page, String template) { + return new FreemarkerTemplateCompiler(page).compile(template); + } + + @Override + public Renderable compileVelocity(Class page, String template) { + return new VelocityTemplateCompiler(page).compile(template); + } + + // TODO(dhanji): Feedback errors as return rather than throwing. + @Override + public void analyze(Class page) { + // May move this into a separate class if it starts getting too big. + analyzeMethods(page.getDeclaredMethods()); + analyzeMethods(page.getMethods()); + } + + private void analyzeMethods(Method[] methods) { + for (Method method : methods) { + for (Annotation annotation : method.getDeclaredAnnotations()) { + // if this is a http method annotation, do some checking on the + // args and return types. + if (httpMethods.containsValue(annotation.annotationType())) { + Class returnType = method.getReturnType(); + + PageBook.Page page = pageBook.forClass(returnType); + if (null == page) { + // throw an error. + } else { + // do further analysis on this sucka + if (page.getUri().contains(":")) + ; // throw an error coz we cant redir to dynamic URLs + + // If this is headless, it MUST return an instance of reply. + if (page.isHeadless()) { + if (!Reply.class.isAssignableFrom(method.getReturnType())) { + // throw error + } + } + } + } } - } } - } } - } - - public void compilePage(PageBook.Page page) { - // find the template page class - Class templateClass = page.pageClass(); - - // root page uses the last template, extension uses its own embedded template - if (!page.isDecorated() && templateClass.isAnnotationPresent(Decorated.class)) { - // the first superclass with a @Show and no @Extension is the template - while (!templateClass.isAnnotationPresent(Show.class) || - templateClass.isAnnotationPresent(Decorated.class)) { - templateClass = templateClass.getSuperclass(); - if (templateClass == Object.class) { - throw new MissingTemplateException("Could not find tempate for " + page.pageClass() + - ". You must use @Show on a superclass of an @Extension page"); + + @Override + public void compilePage(PageBook.Page page) { + // find the template page class + Class templateClass = page.pageClass(); + + // root page uses the last template, extension uses its own embedded template + if (!page.isDecorated() && templateClass.isAnnotationPresent(Decorated.class)) { + // the first superclass with a @Show and no @Extension is the template + while (!templateClass.isAnnotationPresent(Show.class) || templateClass.isAnnotationPresent(Decorated.class)) { + templateClass = templateClass.getSuperclass(); + if (templateClass == Object.class) { + throw new MissingTemplateException("Could not find tempate for " + page.pageClass() + + ". You must use @Show on a superclass of an @Extension page"); + } + } } - } + + Renderable widget = compile(templateClass); + + //apply the compiled widget chain to the page (completing compile step) + page.apply(widget); } - Renderable widget = compile(templateClass); - - //apply the compiled widget chain to the page (completing compile step) - page.apply(widget); - } - - @Override - public Renderable compile(Class templateClass) { - final Template template = loader.load(templateClass); - - Renderable widget; - - //is this an HTML, XML, or a flat-file template? - switch(template.getKind()) { - default: - case HTML: - widget = compileHtml(templateClass, template.getText()); - break; - case XML: - widget = compileXml(templateClass, template.getText()); - break; - case FLAT: - widget = compileFlat(templateClass, template.getText()); - break; - case MVEL: - widget = compileMvel(templateClass, template.getText()); - break; - case FREEMARKER: - widget = compileFreemarker(templateClass, template.getText()); - break; + @Override + public Renderable compile(Class templateClass) { + final Template template = loader.load(templateClass); + + Renderable widget; + + //is this an HTML, XML, or a flat-file template? + switch (template.getKind()) { + default: + case HTML: + widget = compileHtml(templateClass, template.getText()); + break; + case XML: + widget = compileXml(templateClass, template.getText()); + break; + case FLAT: + widget = compileFlat(templateClass, template.getText()); + break; + case MVEL: + widget = compileMvel(templateClass, template.getText()); + break; + case FREEMARKER: + widget = compileFreemarker(templateClass, template.getText()); + break; + case VELOCITY: + widget = compileVelocity(templateClass, template.getText()); + } + return widget; } - return widget; - } } diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java new file mode 100644 index 00000000..3984a55b --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java @@ -0,0 +1,54 @@ +package com.google.sitebricks.compiler.template; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Properties; +import java.util.Set; + +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; + +import com.google.common.collect.ImmutableSet; +import com.google.sitebricks.Renderable; +import com.google.sitebricks.Respond; + +public class VelocityTemplateCompiler { + + private final Class page; + + public VelocityTemplateCompiler(Class page) { + this.page = page; + System.out.println("****** velocity compiler"); + } + + public Renderable compile(final String templateContent) { + //Velocity.init("velocity.properties"); + Properties properties = new Properties(); + try { + properties.load(getClass().getResourceAsStream("/velocity.properties")); + } catch (IOException e) { + throw new RuntimeException(e); + } + + final VelocityEngine velocityEngine = new VelocityEngine(properties); + + final VelocityContext context = new VelocityContext(); + + return new Renderable() { + + @Override + public void render(Object bound, Respond respond) { + System.out.println("********** hold your breath, velocity template being rendered."); + context.put("page", bound); + StringWriter writer = new StringWriter(); + velocityEngine.evaluate(context, writer, "", templateContent); + respond.write(writer.toString()); + } + + @Override + public Set collect(Class clazz) { + return ImmutableSet.of(); + } + }; + } +} diff --git a/sitebricks/src/main/resources/velocity.properties b/sitebricks/src/main/resources/velocity.properties new file mode 100644 index 00000000..3ca9dd56 --- /dev/null +++ b/sitebricks/src/main/resources/velocity.properties @@ -0,0 +1,2 @@ +resource.loader = class +class.resource.loader.class = org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader \ No newline at end of file From 4e9c2a02727fa8be6000533486922068f5c65137 Mon Sep 17 00:00:00 2001 From: james Date: Mon, 12 Dec 2011 14:21:07 -0600 Subject: [PATCH 05/20] a bit of cleanup, no velocity.properties in sitebricks --- .../src/main/resources/velocity.properties | 0 .../google/sitebricks/compiler/StandardCompilers.java | 2 +- .../compiler/template/VelocityTemplateCompiler.java | 11 ++--------- 3 files changed, 3 insertions(+), 10 deletions(-) rename {sitebricks => sitebricks-acceptance-tests}/src/main/resources/velocity.properties (100%) diff --git a/sitebricks/src/main/resources/velocity.properties b/sitebricks-acceptance-tests/src/main/resources/velocity.properties similarity index 100% rename from sitebricks/src/main/resources/velocity.properties rename to sitebricks-acceptance-tests/src/main/resources/velocity.properties diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java index a571a237..4560b5a3 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java @@ -73,7 +73,7 @@ public Renderable compileFreemarker(Class page, String template) { @Override public Renderable compileVelocity(Class page, String template) { - return new VelocityTemplateCompiler(page).compile(template); + return new VelocityTemplateCompiler().compile(template); } // TODO(dhanji): Feedback errors as return rather than throwing. diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java index 3984a55b..3f5143de 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java @@ -14,15 +14,10 @@ public class VelocityTemplateCompiler { - private final Class page; - - public VelocityTemplateCompiler(Class page) { - this.page = page; - System.out.println("****** velocity compiler"); + static { } public Renderable compile(final String templateContent) { - //Velocity.init("velocity.properties"); Properties properties = new Properties(); try { properties.load(getClass().getResourceAsStream("/velocity.properties")); @@ -32,13 +27,11 @@ public Renderable compile(final String templateContent) { final VelocityEngine velocityEngine = new VelocityEngine(properties); - final VelocityContext context = new VelocityContext(); - return new Renderable() { @Override public void render(Object bound, Respond respond) { - System.out.println("********** hold your breath, velocity template being rendered."); + final VelocityContext context = new VelocityContext(); context.put("page", bound); StringWriter writer = new StringWriter(); velocityEngine.evaluate(context, writer, "", templateContent); From 4b177b138bb977899831a2e281b6631654cf5f0b Mon Sep 17 00:00:00 2001 From: james Date: Mon, 12 Dec 2011 14:22:04 -0600 Subject: [PATCH 06/20] ignore this 'DS_Store' directory that popped in from somewhere --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b35da380..9e2cc04f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ out/ # Other bin/ +.DS_Store From a006768edaefdbb97bd45f1128d82602251a4b6d Mon Sep 17 00:00:00 2001 From: james Date: Tue, 13 Dec 2011 07:57:34 -0600 Subject: [PATCH 07/20] AERO-245 update the pom files to set their distributionManagement repositories to cerberus.local --- pom.xml | 12 +++++------- sitebricks-acceptance-tests/pom.xml | 2 +- sitebricks-client/pom.xml | 2 +- sitebricks-converter/pom.xml | 2 +- sitebricks-jetty-archetype/pom.xml | 11 +++++++++++ sitebricks-mail/pom.xml | 11 +++++++++++ sitebricks-options/pom.xml | 11 +++++++++++ sitebricks/pom.xml | 12 +++++------- .../compiler/template/VelocityTemplateCompiler.java | 5 ++++- slf4j/pom.xml | 10 ++++------ stat/pom.xml | 12 +++++------- 11 files changed, 59 insertions(+), 31 deletions(-) diff --git a/pom.xml b/pom.xml index 10e2cc95..2a2bd5a6 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ com.google.sitebricks sitebricks-parent pom - 0.8.5 + 0.8.5C Sitebricks :: Parent https://github.com/dhanji/sitebricks @@ -276,14 +276,12 @@ - google-snapshots - Sonatype OSS Nexus Snapshots - https://oss.sonatype.org/content/repositories/google-snapshots + ext-snapshot-local + http://cerberus.local:8080/artifactory/ext-snapshot-local - google-with-staging - Nexus OSS Staging Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + ext-release-local + http://cerberus.local:8080/artifactory/ext-release-local diff --git a/sitebricks-acceptance-tests/pom.xml b/sitebricks-acceptance-tests/pom.xml index ed24efda..7262d2e8 100644 --- a/sitebricks-acceptance-tests/pom.xml +++ b/sitebricks-acceptance-tests/pom.xml @@ -4,7 +4,7 @@ com.google.sitebricks sitebricks-parent - 0.8.5 + 0.8.5C sitebricks-acceptance-tests Sitebricks :: Acceptance Tests diff --git a/sitebricks-client/pom.xml b/sitebricks-client/pom.xml index 9db9dda8..d145c921 100644 --- a/sitebricks-client/pom.xml +++ b/sitebricks-client/pom.xml @@ -3,7 +3,7 @@ com.google.sitebricks sitebricks-parent - 0.8.5 + 0.8.5C sitebricks-client Sitebricks :: Client diff --git a/sitebricks-converter/pom.xml b/sitebricks-converter/pom.xml index e77cbe57..ddb70403 100644 --- a/sitebricks-converter/pom.xml +++ b/sitebricks-converter/pom.xml @@ -3,7 +3,7 @@ com.google.sitebricks sitebricks-parent - 0.8.5 + 0.8.5C sitebricks-converter Sitebricks :: Type Conversion diff --git a/sitebricks-jetty-archetype/pom.xml b/sitebricks-jetty-archetype/pom.xml index a5fbd730..fed868f0 100644 --- a/sitebricks-jetty-archetype/pom.xml +++ b/sitebricks-jetty-archetype/pom.xml @@ -36,4 +36,15 @@ logback-classic + + + + ext-snapshot-local + http://cerberus.local:8080/artifactory/ext-snapshot-local + + + ext-release-local + http://cerberus.local:8080/artifactory/ext-release-local + + diff --git a/sitebricks-mail/pom.xml b/sitebricks-mail/pom.xml index def65551..a29d2d1d 100644 --- a/sitebricks-mail/pom.xml +++ b/sitebricks-mail/pom.xml @@ -78,4 +78,15 @@ + + + + ext-snapshot-local + http://cerberus.local:8080/artifactory/ext-snapshot-local + + + ext-release-local + http://cerberus.local:8080/artifactory/ext-release-local + + diff --git a/sitebricks-options/pom.xml b/sitebricks-options/pom.xml index c3c552d8..51e8102e 100644 --- a/sitebricks-options/pom.xml +++ b/sitebricks-options/pom.xml @@ -63,4 +63,15 @@ + + + + ext-snapshot-local + http://cerberus.local:8080/artifactory/ext-snapshot-local + + + ext-release-local + http://cerberus.local:8080/artifactory/ext-release-local + + diff --git a/sitebricks/pom.xml b/sitebricks/pom.xml index e4875395..d139d294 100644 --- a/sitebricks/pom.xml +++ b/sitebricks/pom.xml @@ -3,7 +3,7 @@ com.google.sitebricks sitebricks-parent - 0.8.5 + 0.8.5C sitebricks Sitebricks :: Core @@ -131,14 +131,12 @@ - google-snapshots - Sonatype OSS Nexus Snapshots - https://oss.sonatype.org/content/repositories/google-snapshots + ext-snapshot-local + http://cerberus.local:8080/artifactory/ext-snapshot-local - google-with-staging - Nexus OSS Staging Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + ext-release-local + http://cerberus.local:8080/artifactory/ext-release-local diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java index 3f5143de..51bcefbb 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java @@ -1,6 +1,7 @@ package com.google.sitebricks.compiler.template; import java.io.IOException; +import java.io.InputStream; import java.io.StringWriter; import java.util.Properties; import java.util.Set; @@ -20,7 +21,9 @@ public class VelocityTemplateCompiler { public Renderable compile(final String templateContent) { Properties properties = new Properties(); try { - properties.load(getClass().getResourceAsStream("/velocity.properties")); + InputStream propertyStream = getClass().getResourceAsStream("/velocity.properties"); + if (propertyStream != null) + properties.load(propertyStream); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/slf4j/pom.xml b/slf4j/pom.xml index bb1b837e..fd9ecbf6 100644 --- a/slf4j/pom.xml +++ b/slf4j/pom.xml @@ -44,14 +44,12 @@ - google-snapshots - Sonatype OSS Nexus Snapshots - https://oss.sonatype.org/content/repositories/google-snapshots + ext-snapshot-local + http://cerberus.local:8080/artifactory/ext-snapshot-local - google-with-staging - Nexus OSS Staging Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + ext-release-local + http://cerberus.local:8080/artifactory/ext-release-local diff --git a/stat/pom.xml b/stat/pom.xml index 492c926a..fd715b1f 100644 --- a/stat/pom.xml +++ b/stat/pom.xml @@ -3,7 +3,7 @@ com.google.sitebricks sitebricks-parent - 0.8.5 + 0.8.5C sitebricks-stat Sitebricks :: Statistics @@ -76,14 +76,12 @@ - google-snapshots - Sonatype OSS Nexus Snapshots - https://oss.sonatype.org/content/repositories/google-snapshots + ext-snapshot-local + http://cerberus.local:8080/artifactory/ext-snapshot-local - google-with-staging - Nexus OSS Staging Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + ext-release-local + http://cerberus.local:8080/artifactory/ext-release-local From b3b0204aea41af4825672329b3a6f44a9351a18a Mon Sep 17 00:00:00 2001 From: james Date: Fri, 30 Dec 2011 15:22:24 -0600 Subject: [PATCH 08/20] should now return this for a forward-after-redirect as it will be put in the flashcache --- .../routing/WidgetRoutingDispatcher.java | 242 +++++++++--------- 1 file changed, 127 insertions(+), 115 deletions(-) diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java index aadc1f39..12b6b684 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java @@ -1,5 +1,12 @@ package com.google.sitebricks.routing; +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.jcip.annotations.Immutable; + import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -7,12 +14,8 @@ import com.google.sitebricks.binding.FlashCache; import com.google.sitebricks.binding.RequestBinder; import com.google.sitebricks.headless.HeadlessRenderer; +import com.google.sitebricks.headless.Reply; import com.google.sitebricks.rendering.resource.ResourcesService; -import net.jcip.annotations.Immutable; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; /** * @author Dhanji R. Prasanna (dhanji@gmail.com) @@ -20,124 +23,133 @@ @Immutable @Singleton class WidgetRoutingDispatcher implements RoutingDispatcher { - private final PageBook book; - private final RequestBinder binder; - private final Provider respondProvider; - private final ResourcesService resourcesService; - private final Provider flashCacheProvider; - private final HeadlessRenderer headlessRenderer; - - @Inject - public WidgetRoutingDispatcher(PageBook book, RequestBinder binder, Provider respondProvider, - ResourcesService resourcesService, Provider flashCacheProvider, - HeadlessRenderer headlessRenderer) { - this.headlessRenderer = headlessRenderer; - this.book = book; - this.binder = binder; - this.respondProvider = respondProvider; - this.resourcesService = resourcesService; - this.flashCacheProvider = flashCacheProvider; - } - - public Respond dispatch(HttpServletRequest request, HttpServletResponse response) throws IOException { - String uri = getPathInfo(request); - - //first try dispatching as a static resource service - Respond respond = resourcesService.serve(uri); - - if (null != respond) - return respond; - - // Otherwise try to dispatch as a widget/page - // Check if there is a page chain link sitting here - // for this page. - // NOTE(dhanji): we must use remove, to atomically - // remove the page and process it in one go. It is - // also worth coordinating this with conversation request - // queueing. - // TODO(dhanji): Change flashcache to use temporary cookies instead. - PageBook.Page page = flashCacheProvider.get().remove(uri); - - // If there is no link, obtain page via Guice as normal. - if (null == page) - page = book.get(uri); - - //could not dispatch as there was no match - if (null == page) - return null; - - final Object instance = page.instantiate(); - if (page.isHeadless()) { - respond = Respond.HEADLESS; - - bindAndReply(request, response, page, instance); - } else { - respond = respondProvider.get(); - - //fire events and render reponders - bindAndRespond(request, page, respond, instance); + private final PageBook book; + private final RequestBinder binder; + private final Provider respondProvider; + private final ResourcesService resourcesService; + private final Provider flashCacheProvider; + private final HeadlessRenderer headlessRenderer; + + @Inject + public WidgetRoutingDispatcher(PageBook book, RequestBinder binder, Provider respondProvider, + ResourcesService resourcesService, Provider flashCacheProvider, + HeadlessRenderer headlessRenderer) { + this.headlessRenderer = headlessRenderer; + this.book = book; + this.binder = binder; + this.respondProvider = respondProvider; + this.resourcesService = resourcesService; + this.flashCacheProvider = flashCacheProvider; } - return respond; - } - - private void bindAndReply(HttpServletRequest request, HttpServletResponse response, - PageBook.Page page, Object instance) throws IOException { - // bind request (sets request params, etc). - binder.bind(request, instance); - - // call the appropriate handler. - headlessRenderer.render(response, fireEvent(request, page, instance)); - - } - - private String getPathInfo(HttpServletRequest request) { - return request.getRequestURI().substring(request.getContextPath().length()); - } - - private void bindAndRespond(HttpServletRequest request, PageBook.Page page, Respond respond, - Object instance) { - //bind request - binder.bind(request, instance); - - //fire get/post events - final Object redirect = fireEvent(request, page, instance); + @Override + public Respond dispatch(HttpServletRequest request, HttpServletResponse response) throws IOException { + String uri = getPathInfo(request); + + //first try dispatching as a static resource service + Respond respond = resourcesService.serve(uri); + + if (null != respond) + return respond; + + // Otherwise try to dispatch as a widget/page + // Check if there is a page chain link sitting here + // for this page. + // NOTE(dhanji): we must use remove, to atomically + // remove the page and process it in one go. It is + // also worth coordinating this with conversation request + // queueing. + // TODO(dhanji): Change flashcache to use temporary cookies instead. + PageBook.Page page = flashCacheProvider.get().remove(uri); + + // If there is no link, obtain page via Guice as normal. + if (null == page) + page = book.get(uri); + + //could not dispatch as there was no match + if (null == page) + return null; + + final Object instance = page.instantiate(); + if (page.isHeadless()) { + respond = Respond.HEADLESS; + } else { + //fire events and render reponders + respond = bindAndRespond(request, page, instance, uri); + } + if (respond == Respond.HEADLESS) { + bindAndReply(request, response, page, instance); + } + + return respond; + } - //render to respond - if (null != redirect) { + private void bindAndReply(HttpServletRequest request, HttpServletResponse response, PageBook.Page page, + Object instance) throws IOException { + // bind request (sets request params, etc). + binder.bind(request, instance); - if (redirect instanceof String) - respond.redirect((String) redirect); - else if (redirect instanceof Class) { - PageBook.Page targetPage = book.forClass((Class) redirect); + // call the appropriate handler. + headlessRenderer.render(response, fireEvent(request, page, instance)); - // should never be null coz it is validated on compile. - respond.redirect(contextualize(request, targetPage.getUri())); - } else { - // Handle page-chaining driven redirection. - PageBook.Page targetPage = book.forInstance(redirect); + } - // should never be null coz it will be validated at compile time. - flashCacheProvider.get().put(targetPage.getUri(), targetPage); + private String getPathInfo(HttpServletRequest request) { + return request.getRequestURI().substring(request.getContextPath().length()); + } - // Send to the canonical address of the page. This is also - // verified at compile, not be a variablized matcher. - respond.redirect(contextualize(request, targetPage.getUri())); - } - } else - page.widget().render(instance, respond); - } + private Respond bindAndRespond(HttpServletRequest request, PageBook.Page page, Object instance, String uri) { + Respond respond = respondProvider.get(); + //bind request + binder.bind(request, instance); + + //fire get/post events + final Object redirect = fireEvent(request, page, instance); + + //render to respond + if (null != redirect) { + + if (redirect instanceof String) + respond.redirect((String) redirect); + else if (redirect instanceof Class) { + PageBook.Page targetPage = book.forClass((Class) redirect); + + // should never be null coz it is validated on compile. + respond.redirect(contextualize(request, targetPage.getUri())); + } else if (redirect instanceof Reply) { + return Respond.HEADLESS; + } else { + // Handle page-chaining driven redirection. + PageBook.Page targetPage = book.forInstance(redirect); + String redirectUri = targetPage.getUri(); + if (redirect == instance) { + redirectUri = uri; + targetPage = page; + } + + // should never be null coz it will be validated at compile time. + flashCacheProvider.get().put(redirectUri, targetPage); + + // Send to the canonical address of the page. This is also + // verified at compile, not be a variablized matcher. + respond.redirect(contextualize(request, redirectUri)); + } + } else { + page.widget().render(instance, respond); + } + return respond; + } - // We're sure the request parameter map is a Map - @SuppressWarnings("unchecked") - private Object fireEvent(HttpServletRequest request, PageBook.Page page, Object instance) { - final String method = request.getMethod(); - final String pathInfo = getPathInfo(request); + // We're sure the request parameter map is a Map + @SuppressWarnings("unchecked") + private Object fireEvent(HttpServletRequest request, PageBook.Page page, Object instance) { + final String method = request.getMethod(); + final String pathInfo = getPathInfo(request); - return page.doMethod(method.toLowerCase(), instance, pathInfo, request); - } + return page.doMethod(method.toLowerCase(), instance, pathInfo, request); + } - private static String contextualize(HttpServletRequest request, String targetUri) { - return request.getContextPath() + targetUri; - } + private static String contextualize(HttpServletRequest request, String targetUri) { + return request.getContextPath() + targetUri; + } } From 0c5972a7a9d426bad3faed6f9c58c68cb1f72080 Mon Sep 17 00:00:00 2001 From: james Date: Mon, 30 Jan 2012 09:52:41 -0600 Subject: [PATCH 09/20] AERO-488 update the pom.xml to use nightflight for 'mvn deploy' to artifactory * create VelocityEngineProvider to create just one engine for all template compilations --- pom.xml | 4 +- sitebricks-jetty-archetype/pom.xml | 4 +- sitebricks-mail/pom.xml | 4 +- sitebricks-options/pom.xml | 4 +- sitebricks/pom.xml | 10 +- .../ScanAndCompileBootstrapper.java | 448 +++--- .../compiler/StandardCompilers.java | 8 +- .../template/VelocityEngineProvider.java | 31 + .../template/VelocityTemplateCompiler.java | 24 +- .../sitebricks/routing/DefaultPageBook.java | 1293 +++++++++-------- slf4j/pom.xml | 4 +- stat/pom.xml | 4 +- 12 files changed, 948 insertions(+), 890 deletions(-) create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityEngineProvider.java diff --git a/pom.xml b/pom.xml index 2a2bd5a6..d9e7b2ed 100644 --- a/pom.xml +++ b/pom.xml @@ -277,11 +277,11 @@ ext-snapshot-local - http://cerberus.local:8080/artifactory/ext-snapshot-local + http://nightflight.local:8383/artifactory/ext-snapshot-local ext-release-local - http://cerberus.local:8080/artifactory/ext-release-local + http://nightflight.local:8383/artifactory/ext-release-local diff --git a/sitebricks-jetty-archetype/pom.xml b/sitebricks-jetty-archetype/pom.xml index fed868f0..f643a367 100644 --- a/sitebricks-jetty-archetype/pom.xml +++ b/sitebricks-jetty-archetype/pom.xml @@ -40,11 +40,11 @@ ext-snapshot-local - http://cerberus.local:8080/artifactory/ext-snapshot-local + http://nightflight.local:8383/artifactory/ext-snapshot-local ext-release-local - http://cerberus.local:8080/artifactory/ext-release-local + http://nightflight.local:8383/artifactory/ext-release-local diff --git a/sitebricks-mail/pom.xml b/sitebricks-mail/pom.xml index a29d2d1d..46fb231b 100644 --- a/sitebricks-mail/pom.xml +++ b/sitebricks-mail/pom.xml @@ -82,11 +82,11 @@ ext-snapshot-local - http://cerberus.local:8080/artifactory/ext-snapshot-local + http://nightflight.local:8383/artifactory/ext-snapshot-local ext-release-local - http://cerberus.local:8080/artifactory/ext-release-local + http://nightflight.local:8383/artifactory/ext-release-local diff --git a/sitebricks-options/pom.xml b/sitebricks-options/pom.xml index 51e8102e..c99b83fa 100644 --- a/sitebricks-options/pom.xml +++ b/sitebricks-options/pom.xml @@ -67,11 +67,11 @@ ext-snapshot-local - http://cerberus.local:8080/artifactory/ext-snapshot-local + http://nightflight.local:8383/artifactory/ext-snapshot-local ext-release-local - http://cerberus.local:8080/artifactory/ext-release-local + http://nightflight.local:8383/artifactory/ext-release-local diff --git a/sitebricks/pom.xml b/sitebricks/pom.xml index d139d294..80249516 100644 --- a/sitebricks/pom.xml +++ b/sitebricks/pom.xml @@ -21,6 +21,12 @@ velocity 1.7 + + oro + oro + 2.0.8 + runtime + com.google.sitebricks sitebricks-converter @@ -132,11 +138,11 @@ ext-snapshot-local - http://cerberus.local:8080/artifactory/ext-snapshot-local + http://nightflight.local:8383/artifactory/ext-snapshot-local ext-release-local - http://cerberus.local:8080/artifactory/ext-release-local + http://nightflight.local:8383/artifactory/ext-release-local diff --git a/sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java b/sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java index 50a1a41d..9ff4ecf5 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java +++ b/sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java @@ -1,5 +1,19 @@ package com.google.sitebricks; +import static com.google.inject.matcher.Matchers.annotatedWith; +import static com.google.sitebricks.SitebricksModule.BindingKind.ACTION; +import static com.google.sitebricks.SitebricksModule.BindingKind.EMBEDDED; +import static com.google.sitebricks.SitebricksModule.BindingKind.PAGE; +import static com.google.sitebricks.SitebricksModule.BindingKind.SERVICE; +import static com.google.sitebricks.SitebricksModule.BindingKind.STATIC_RESOURCE; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + import com.google.common.collect.HashBiMap; import com.google.common.collect.Lists; import com.google.common.collect.Sets; @@ -20,264 +34,244 @@ import com.google.sitebricks.routing.PageBook.Page; import com.google.sitebricks.routing.SystemMetrics; -import java.lang.annotation.Annotation; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static com.google.inject.matcher.Matchers.annotatedWith; -import static com.google.sitebricks.SitebricksModule.BindingKind.ACTION; -import static com.google.sitebricks.SitebricksModule.BindingKind.EMBEDDED; -import static com.google.sitebricks.SitebricksModule.BindingKind.PAGE; -import static com.google.sitebricks.SitebricksModule.BindingKind.SERVICE; -import static com.google.sitebricks.SitebricksModule.BindingKind.STATIC_RESOURCE; - /** * @author Dhanji R. Prasanna (dhanji@gmail.com) */ class ScanAndCompileBootstrapper implements Bootstrapper { - private final PageBook pageBook; - private final List packages; - private final ResourcesService resourcesService; - private final WidgetRegistry registry; - private final SystemMetrics metrics; - private final Compilers compilers; - - @Inject - private final Templates templates = null; - - @Inject @Bricks - private final List bindings = null; - - @Inject @Bricks - private final Map> methodMap = null; - - @Inject - private final Stage currentStage = null; - - @Inject - private final Injector injector = null; - - private final Logger log = Logger.getLogger(ScanAndCompileBootstrapper.class.getName()); - - @Inject - public ScanAndCompileBootstrapper(PageBook pageBook, - @Bricks List packages, - ResourcesService resourcesService, - WidgetRegistry registry, - SystemMetrics metrics, - Compilers compilers) { - - this.pageBook = pageBook; - this.packages = packages; - this.resourcesService = resourcesService; - this.registry = registry; - - this.metrics = metrics; - this.compilers = compilers; - } - - public void start() { - Set> set = Sets.newHashSet(); - for (Package pkg : packages) { - - //look for any classes annotated with @At, @EmbedAs and @With - set.addAll(Classes.matching( - annotatedWith(At.class).or( - annotatedWith(EmbedAs.class)).or( - annotatedWith(With.class)).or( - annotatedWith(Show.class)) - ).in(pkg)); - } + private final PageBook pageBook; + private final List packages; + private final ResourcesService resourcesService; + private final WidgetRegistry registry; + private final SystemMetrics metrics; + private final Compilers compilers; - //we need to scan all the pages first (do not collapse into the next loop) - Set pagesToCompile = scanPagesToCompile(set); - collectBindings(bindings, pagesToCompile); - extendedPages(pagesToCompile); + @Inject + private final Templates templates = null; - // Compile templates for scanned classes (except in dev mode, where faster startup - // time is more important and compiles are amortized across visits to each page). - // TODO make this configurable separately to stage for GAE - if (Stage.DEVELOPMENT != currentStage) { - compilePages(pagesToCompile); - } + @Inject + @Bricks + private final List bindings = null; - // Start all services. - List> bindings = injector.findBindingsByType(AWARE_TYPE); - for (Binding binding : bindings) { - binding.getProvider().get().startup(); - } + @Inject + @Bricks + private final Map> methodMap = null; - //set application mode to started (now debug mechanics can kick in) - metrics.activate(); - } + @Inject + private final Stage currentStage = null; - private void extendedPages(Set pagesToCompile) { - for (Page page : pagesToCompile) { - if (page.pageClass().isAnnotationPresent(Decorated.class)) { - // recursively add extension pages - analyseExtension(pagesToCompile, page.pageClass()); - } - } - } + @Inject + private final Injector injector = null; - //processes all explicit bindings, including static resources. - private void collectBindings(List bindings, - Set pagesToCompile) { + private final Logger log = Logger.getLogger(ScanAndCompileBootstrapper.class.getName()); - // Reverse the method map for easy lookup of HTTP method annotations. - Map, String> methodSet = null; + @Inject + public ScanAndCompileBootstrapper(PageBook pageBook, @Bricks List packages, + ResourcesService resourcesService, WidgetRegistry registry, SystemMetrics metrics, Compilers compilers) { - //go thru bindings and obtain pages from them. - for (SitebricksModule.LinkingBinder binding : bindings) { + this.pageBook = pageBook; + this.packages = packages; + this.resourcesService = resourcesService; + this.registry = registry; - if (EMBEDDED == binding.bindingKind) { - if (null == binding.embedAs) { - // This can happen if embed() is not followed by an .as(..) - throw new IllegalStateException("embed() missing .as() clause: " + binding.pageClass); - } - registry.addEmbed(binding.embedAs); - pagesToCompile.add(pageBook.embedAs(binding.pageClass, binding.embedAs)); - - } else if (PAGE == binding.bindingKind) { - pagesToCompile.add(pageBook.at(binding.uri, binding.pageClass)); - - } else if (STATIC_RESOURCE == binding.bindingKind) { - //localize the resource to the SitebricksModule's package. - resourcesService.add(SitebricksModule.class, binding.getResource()); - } else if (SERVICE == binding.bindingKind) { - pagesToCompile.add(pageBook.serviceAt(binding.uri, binding.pageClass)); - } else if (ACTION == binding.bindingKind) { - // Lazy create this inverse lookup map, once. - if (null == methodSet) { - methodSet = HashBiMap.create(methodMap).inverse(); - } - pageBook.at(binding.uri, binding.actionDescriptors, methodSet); - } + this.metrics = metrics; + this.compilers = compilers; } - } - - //goes through the set of scanned classes and builds pages out of them. - private Set scanPagesToCompile(Set> set) { - Set templates = Sets.newHashSet(); - Set pagesToCompile = Sets.newHashSet(); - for (Class pageClass : set) { - EmbedAs embedAs = pageClass.getAnnotation(EmbedAs.class); - if (null != embedAs) { - final String embedName = embedAs.value(); - - //is this a text rendering or embedding-style widget? - if (Renderable.class.isAssignableFrom(pageClass)) { - @SuppressWarnings("unchecked") - Class renderable = (Class) pageClass; - registry.add(embedName, renderable); - } else { - pagesToCompile.add(embed(embedName, pageClass)); + + @Override + public void start() { + Set> set = Sets.newHashSet(); + for (Package pkg : packages) { + + //look for any classes annotated with @At, @EmbedAs and @With + set.addAll(Classes.matching( + annotatedWith(At.class).or(annotatedWith(EmbedAs.class)).or(annotatedWith(With.class)) + .or(annotatedWith(Show.class))).in(pkg)); } - } - - At at = pageClass.getAnnotation(At.class); - if (null != at) { - if (pageClass.isAnnotationPresent(Service.class)) { - pagesToCompile.add(pageBook.serviceAt(at.value(), pageClass)); - } else if (pageClass.isAnnotationPresent(Export.class)) { - //localize the resource to the SitebricksModule's package. - resourcesService.add(SitebricksModule.class, pageClass.getAnnotation(Export.class)); + + //we need to scan all the pages first (do not collapse into the next loop) + Set pagesToCompile = scanPagesToCompile(set); + collectBindings(bindings, pagesToCompile); + extendedPages(pagesToCompile); + + // Compile templates for scanned classes (except in dev mode, where faster startup + // time is more important and compiles are amortized across visits to each page). + // TODO make this configurable separately to stage for GAE + if (Stage.DEVELOPMENT != currentStage) { + compilePages(pagesToCompile); } - else { - pagesToCompile.add(pageBook.at(at.value(), pageClass)); + + // Start all services. + List> bindings = injector.findBindingsByType(AWARE_TYPE); + for (Binding binding : bindings) { + binding.getProvider().get().startup(); } - } - if (pageClass.isAnnotationPresent(Show.class)) { - // This has a template associated with it. - templates.add(new Templates.Descriptor(pageClass, - pageClass.getAnnotation(Show.class).value())); - } + //set application mode to started (now debug mechanics can kick in) + metrics.activate(); } - // Eagerly load all detected templates in production mode. - if (Stage.DEVELOPMENT != currentStage) { - this.templates.loadAll(templates); + private void extendedPages(Set pagesToCompile) { + Set pageExtensions = Sets.newHashSet(); + for (Page page : pagesToCompile) { + if (page.pageClass().isAnnotationPresent(Decorated.class)) { + // recursively add extension pages + analyseExtension(pageExtensions, page.pageClass()); + } + } + pagesToCompile.addAll(pageExtensions); } - return pagesToCompile; - } - - private void analyseExtension(Set pagesToCompile, Class extendClass) { - // store the page with a special page name used by ExtendWidget - pagesToCompile.add(pageBook.decorate(extendClass)); - - // recursively analyse super class - while (extendClass != Object.class) { - extendClass = extendClass.getSuperclass(); - if (extendClass.isAnnotationPresent(Decorated.class)) { - analyseExtension(pagesToCompile, extendClass); - } - else if (extendClass.isAnnotationPresent(Show.class)) { - // there is a @Show with no @Extension so this is the outer template - return; - } + //processes all explicit bindings, including static resources. + private void collectBindings(List bindings, Set pagesToCompile) { + + // Reverse the method map for easy lookup of HTTP method annotations. + Map, String> methodSet = null; + + //go thru bindings and obtain pages from them. + for (SitebricksModule.LinkingBinder binding : bindings) { + + if (EMBEDDED == binding.bindingKind) { + if (null == binding.embedAs) { + // This can happen if embed() is not followed by an .as(..) + throw new IllegalStateException("embed() missing .as() clause: " + binding.pageClass); + } + registry.addEmbed(binding.embedAs); + pagesToCompile.add(pageBook.embedAs(binding.pageClass, binding.embedAs)); + + } else if (PAGE == binding.bindingKind) { + pagesToCompile.add(pageBook.at(binding.uri, binding.pageClass)); + + } else if (STATIC_RESOURCE == binding.bindingKind) { + //localize the resource to the SitebricksModule's package. + resourcesService.add(SitebricksModule.class, binding.getResource()); + } else if (SERVICE == binding.bindingKind) { + pagesToCompile.add(pageBook.serviceAt(binding.uri, binding.pageClass)); + } else if (ACTION == binding.bindingKind) { + // Lazy create this inverse lookup map, once. + if (null == methodSet) { + methodSet = HashBiMap.create(methodMap).inverse(); + } + pageBook.at(binding.uri, binding.actionDescriptors, methodSet); + } + } } - throw new IllegalStateException("Could not find super class annotated with @Show"); - } - - private void compilePages(Set pagesToCompile) { - final List failures = Lists.newArrayList(); - - //perform a compilation pass over all the pages and their templates - for (PageBook.Page page : pagesToCompile) { - Class pageClass = page.pageClass(); - - // Headless web services need to be analyzed but not page-compiled. - if (page.isHeadless()) { - // TODO(dhanji): Feedback errors as return rather than throwing. - compilers.analyze(pageClass); - continue; - } - - if (log.isLoggable(Level.FINEST)) { - log.finest("Compiling template for page " + pageClass.getName()); - } - - try { - compilers.compilePage(page); - compilers.analyze(pageClass); - } catch (TemplateCompileException e) { - failures.add(e); - } + + //goes through the set of scanned classes and builds pages out of them. + private Set scanPagesToCompile(Set> set) { + Set templates = Sets.newHashSet(); + Set pagesToCompile = Sets.newHashSet(); + for (Class pageClass : set) { + EmbedAs embedAs = pageClass.getAnnotation(EmbedAs.class); + if (null != embedAs) { + final String embedName = embedAs.value(); + + //is this a text rendering or embedding-style widget? + if (Renderable.class.isAssignableFrom(pageClass)) { + @SuppressWarnings("unchecked") + Class renderable = (Class) pageClass; + registry.add(embedName, renderable); + } else { + pagesToCompile.add(embed(embedName, pageClass)); + } + } + + At at = pageClass.getAnnotation(At.class); + if (null != at) { + if (pageClass.isAnnotationPresent(Service.class)) { + pagesToCompile.add(pageBook.serviceAt(at.value(), pageClass)); + } else if (pageClass.isAnnotationPresent(Export.class)) { + //localize the resource to the SitebricksModule's package. + resourcesService.add(SitebricksModule.class, pageClass.getAnnotation(Export.class)); + } else { + pagesToCompile.add(pageBook.at(at.value(), pageClass)); + } + } + + if (pageClass.isAnnotationPresent(Show.class)) { + // This has a template associated with it. + templates.add(new Templates.Descriptor(pageClass, pageClass.getAnnotation(Show.class).value())); + } + } + + // Eagerly load all detected templates in production mode. + if (Stage.DEVELOPMENT != currentStage) { + this.templates.loadAll(templates); + } + + return pagesToCompile; } - //log failures if any (we don't abort the app startup) - if (!failures.isEmpty()) { - logFailures(failures); + private void analyseExtension(Set pagesToCompile, Class extendClass) { + // store the page with a special page name used by ExtendWidget + pagesToCompile.add(pageBook.decorate(extendClass)); + + // recursively analyse super class + while (extendClass != Object.class) { + extendClass = extendClass.getSuperclass(); + if (extendClass.isAnnotationPresent(Decorated.class)) { + analyseExtension(pagesToCompile, extendClass); + } else if (extendClass.isAnnotationPresent(Show.class)) { + // there is a @Show with no @Extension so this is the outer template + return; + } + } + throw new IllegalStateException("Could not find super class annotated with @Show"); } - } - private PageBook.Page embed(String embedAs, Class page) { - //store custom page wrapped as an embed widget - registry.addEmbed(embedAs); + private void compilePages(Set pagesToCompile) { + final List failures = Lists.newArrayList(); + + //perform a compilation pass over all the pages and their templates + for (PageBook.Page page : pagesToCompile) { + Class pageClass = page.pageClass(); + + // Headless web services need to be analyzed but not page-compiled. + if (page.isHeadless()) { + // TODO(dhanji): Feedback errors as return rather than throwing. + compilers.analyze(pageClass); + continue; + } + + if (log.isLoggable(Level.FINEST)) { + log.finest("Compiling template for page " + pageClass.getName()); + } + + try { + compilers.compilePage(page); + compilers.analyze(pageClass); + } catch (TemplateCompileException e) { + failures.add(e); + } + } - //store argument name(s) wrapped as an Argument (multiple aliases allowed) - if (page.isAnnotationPresent(With.class)) { - for (String callWith : page.getAnnotation(With.class).value()) { - registry.addArgument(callWith); - } + //log failures if any (we don't abort the app startup) + if (!failures.isEmpty()) { + logFailures(failures); + } } - //...add as an unbound (to URI) page - return pageBook.embedAs(page, embedAs); - } + private PageBook.Page embed(String embedAs, Class page) { + //store custom page wrapped as an embed widget + registry.addEmbed(embedAs); + + //store argument name(s) wrapped as an Argument (multiple aliases allowed) + if (page.isAnnotationPresent(With.class)) { + for (String callWith : page.getAnnotation(With.class).value()) { + registry.addArgument(callWith); + } + } - private void logFailures(List failures) { - StringBuilder builder = new StringBuilder(); - for (TemplateCompileException failure : failures) { - builder.append(failure.getMessage()); - builder.append("\n\n"); + //...add as an unbound (to URI) page + return pageBook.embedAs(page, embedAs); } - log.severe(builder.toString()); - } + private void logFailures(List failures) { + StringBuilder builder = new StringBuilder(); + for (TemplateCompileException failure : failures) { + builder.append(failure.getMessage()); + builder.append("\n\n"); + } + + log.severe(builder.toString()); + } } diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java index 4560b5a3..70e829bb 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java @@ -13,6 +13,7 @@ import com.google.sitebricks.Template; import com.google.sitebricks.TemplateLoader; import com.google.sitebricks.compiler.template.MvelTemplateCompiler; +import com.google.sitebricks.compiler.template.VelocityEngineProvider; import com.google.sitebricks.compiler.template.VelocityTemplateCompiler; import com.google.sitebricks.compiler.template.freemarker.FreemarkerTemplateCompiler; import com.google.sitebricks.headless.Reply; @@ -33,15 +34,18 @@ class StandardCompilers implements Compilers { private final SystemMetrics metrics; private final Map> httpMethods; private final TemplateLoader loader; + private final VelocityEngineProvider velocityEngineProvider; @Inject public StandardCompilers(WidgetRegistry registry, PageBook pageBook, SystemMetrics metrics, - @Bricks Map> httpMethods, TemplateLoader loader) { + @Bricks Map> httpMethods, TemplateLoader loader, + VelocityEngineProvider velocityEngineProvider) { this.registry = registry; this.pageBook = pageBook; this.metrics = metrics; this.httpMethods = httpMethods; this.loader = loader; + this.velocityEngineProvider = velocityEngineProvider; } @Override @@ -73,7 +77,7 @@ public Renderable compileFreemarker(Class page, String template) { @Override public Renderable compileVelocity(Class page, String template) { - return new VelocityTemplateCompiler().compile(template); + return new VelocityTemplateCompiler(velocityEngineProvider).compile(template); } // TODO(dhanji): Feedback errors as return rather than throwing. diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityEngineProvider.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityEngineProvider.java new file mode 100644 index 00000000..28d947d0 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityEngineProvider.java @@ -0,0 +1,31 @@ +package com.google.sitebricks.compiler.template; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import javax.inject.Singleton; + +import org.apache.velocity.app.VelocityEngine; + +import com.google.inject.Provider; + + +public class VelocityEngineProvider implements Provider { + + @Override + @Singleton + public VelocityEngine get() { + Properties properties = new Properties(); + try { + InputStream propertyStream = getClass().getResourceAsStream("/velocity.properties"); + if (propertyStream != null) + properties.load(propertyStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return new VelocityEngine(properties); + } + +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java index 51bcefbb..f7593f5d 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java @@ -1,13 +1,11 @@ package com.google.sitebricks.compiler.template; -import java.io.IOException; -import java.io.InputStream; import java.io.StringWriter; -import java.util.Properties; import java.util.Set; +import javax.inject.Inject; + import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.VelocityEngine; import com.google.common.collect.ImmutableSet; import com.google.sitebricks.Renderable; @@ -15,20 +13,14 @@ public class VelocityTemplateCompiler { - static { + private final VelocityEngineProvider provider; + + @Inject + public VelocityTemplateCompiler(VelocityEngineProvider provider) { + this.provider = provider; } public Renderable compile(final String templateContent) { - Properties properties = new Properties(); - try { - InputStream propertyStream = getClass().getResourceAsStream("/velocity.properties"); - if (propertyStream != null) - properties.load(propertyStream); - } catch (IOException e) { - throw new RuntimeException(e); - } - - final VelocityEngine velocityEngine = new VelocityEngine(properties); return new Renderable() { @@ -37,7 +29,7 @@ public void render(Object bound, Respond respond) { final VelocityContext context = new VelocityContext(); context.put("page", bound); StringWriter writer = new StringWriter(); - velocityEngine.evaluate(context, writer, "", templateContent); + provider.get().evaluate(context, writer, "", templateContent); respond.write(writer.toString()); } diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java b/sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java index 24954c3e..a3417130 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java @@ -1,5 +1,26 @@ package com.google.sitebricks.routing; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReference; + +import javax.servlet.http.HttpServletRequest; + +import net.jcip.annotations.GuardedBy; +import net.jcip.annotations.ThreadSafe; + +import org.jetbrains.annotations.Nullable; + +import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; @@ -24,755 +45,765 @@ import com.google.sitebricks.http.negotiate.Negotiation; import com.google.sitebricks.rendering.Strings; import com.google.sitebricks.rendering.control.DecorateWidget; -import net.jcip.annotations.GuardedBy; -import net.jcip.annotations.ThreadSafe; -import org.jetbrains.annotations.Nullable; - -import javax.servlet.http.HttpServletRequest; -import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicReference; /** * contains active uri/widget mappings * * @author Dhanji R. Prasanna (dhanji@gmail.com) */ -@ThreadSafe @Singleton +@ThreadSafe +@Singleton public class DefaultPageBook implements PageBook { - //multimaps TODO refactor to multimap? - - @GuardedBy("lock") // All three following fields - private final Map> pages = Maps.newHashMap(); - private final List universalMatchingPages = Lists.newArrayList(); - private final Map pagesByName = Maps.newHashMap(); - - private final ConcurrentMap, PageTuple> classToPageMap = - new MapMaker() - .weakKeys() - .weakValues() - .makeMap(); - - private final Object lock = new Object(); - private final Injector injector; - - @Inject - public DefaultPageBook(Injector injector) { - this.injector = injector; - } - - @Override @SuppressWarnings("unchecked") - public Collection> getPageMap() { - return (Collection) pages.values(); - } - - public Page serviceAt(String uri, Class pageClass) { - // Handle subpaths, registering each as a separate instance of the page - // tuple. - for (Method method : pageClass.getDeclaredMethods()) { - if (method.isAnnotationPresent(At.class)) { - - // This is a subpath expression. - At at = method.getAnnotation(At.class); - String subpath = at.value(); - - // Validate subpath - if (!subpath.startsWith("/") || subpath.isEmpty() || subpath.length() == 1) { - throw new IllegalArgumentException(String.format( - "Subpath At(\"%s\") on %s.%s() must begin with a \"/\" and must not be empty", - subpath, pageClass.getName(), method.getName())); - } - - subpath = uri + subpath; - - // Register as headless web service. - at(subpath, pageClass, true); - } - } - - return at(uri, pageClass, true); - } - - public PageTuple at(String uri, Class clazz) { - return at(uri, clazz, clazz.isAnnotationPresent(Service.class)); - } + //multimaps TODO refactor to multimap? - @Override - public void at(String uri, List actionDescriptors, - Map, String> methodSet) { - Multimap actions = HashMultimap.create(); + @GuardedBy("lock") + // All three following fields + private final Map> pages = Maps.newHashMap(); + private final List universalMatchingPages = Lists.newArrayList(); + private final Map pagesByName = Maps.newHashMap(); - for (ActionDescriptor actionDescriptor : actionDescriptors) { - for (Class method : actionDescriptor.getMethods()) { - String methodString = methodSet.get(method); - Action action = actionDescriptor.getAction(); + private final ConcurrentMap, PageTuple> classToPageMap = new MapMaker().weakKeys().weakValues().makeMap(); - if (null == action) { - action = injector.getInstance(actionDescriptor.getActionKey()); - } else { - injector.injectMembers(action); - } - - actions.put(methodString, new SpiAction(action, actionDescriptor)); - } - } + private final Object lock = new Object(); + private final Injector injector; - // Register into the book! - at(new PageTuple(uri, new PathMatcherChain(uri), null, true, false, injector, actions)); - } - - private void at(PageTuple page) { - // Is Universal? - synchronized (lock) { - String key = firstPathElement(page.getUri()); - if (isVariable(key)) { - universalMatchingPages.add(page); - } else { - multiput(pages, key, page); - } + @Inject + public DefaultPageBook(Injector injector) { + this.injector = injector; } - // Actions are not backed by classes. - if (page.pageClass() != null) - classToPageMap.put(page.pageClass(), page); - } - - private PageTuple at(String uri, Class clazz, boolean headless) { - final String key = firstPathElement(uri); - final PageTuple pageTuple = - new PageTuple(uri, new PathMatcherChain(uri), clazz, injector, headless, false); - - synchronized (lock) { - //is universal? (i.e. first element is a variable) - if (isVariable(key)) - universalMatchingPages.add(pageTuple); - else { - multiput(pages, key, pageTuple); - } + @Override + @SuppressWarnings("unchecked") + public Collection> getPageMap() { + return (Collection) pages.values(); } - // Does not need to be inside lock, as it is concurrent. - classToPageMap.put(clazz, pageTuple); + @Override + public Page serviceAt(String uri, Class pageClass) { + // Handle subpaths, registering each as a separate instance of the page + // tuple. + for (Method method : pageClass.getDeclaredMethods()) { + if (method.isAnnotationPresent(At.class)) { + + // This is a subpath expression. + At at = method.getAnnotation(At.class); + String subpath = at.value(); + + // Validate subpath + if (!subpath.startsWith("/") || subpath.isEmpty() || subpath.length() == 1) { + throw new IllegalArgumentException(String.format( + "Subpath At(\"%s\") on %s.%s() must begin with a \"/\" and must not be empty", subpath, + pageClass.getName(), method.getName())); + } - return pageTuple; - } + subpath = uri + subpath; - public Page embedAs(Class clazz, String as) { - Preconditions.checkArgument(null == clazz.getAnnotation(Service.class), - "You cannot embed headless web services!"); - PageTuple pageTuple = new PageTuple("", PathMatcherChain.ignoring(), clazz, injector, false, false); + // Register as headless web service. + at(subpath, pageClass, true); + } + } - synchronized (lock) { - pagesByName.put(as.toLowerCase(), pageTuple); + return at(uri, pageClass, true); } - return pageTuple; - } - - public Page decorate(Class pageClass) { - Preconditions.checkArgument(null == pageClass.getAnnotation(Service.class), - "You cannot extend headless web services!"); - PageTuple pageTuple = new PageTuple("", PathMatcherChain.ignoring(), pageClass, injector, false, true); - - // store page with a special name used by ExtendWidget - String name = DecorateWidget.embedNameFor(pageClass); - synchronized (lock) { - pagesByName.put(name, pageTuple); - } - - return pageTuple; - } - - public Page nonCompilingGet(String uri) { - // The regular get is non compiling, in our case. So these methods are identical. - return get(uri); - } - - private static void multiput(Map> pages, String key, - PageTuple page) { - List list = pages.get(key); - - if (null == list) { - list = new ArrayList(); - pages.put(key, list); + @Override + public PageTuple at(String uri, Class clazz) { + return at(uri, clazz, clazz.isAnnotationPresent(Service.class)); } - list.add(page); - } - - private static boolean isVariable(String key) { - return key.length() > 0 && ':' == key.charAt(0); - } + @Override + public void at(String uri, List actionDescriptors, + Map, String> methodSet) { + Multimap actions = HashMultimap.create(); + + for (ActionDescriptor actionDescriptor : actionDescriptors) { + for (Class method : actionDescriptor.getMethods()) { + String methodString = methodSet.get(method); + Action action = actionDescriptor.getAction(); + + if (null == action) { + action = injector.getInstance(actionDescriptor.getActionKey()); + } else { + injector.injectMembers(action); + } - String firstPathElement(String uri) { - String shortUri = uri.substring(1); + actions.put(methodString, new SpiAction(action, actionDescriptor)); + } + } - final int index = shortUri.indexOf("/"); + // Register into the book! + at(new PageTuple(uri, new PathMatcherChain(uri), null, true, false, injector, actions)); + } - return (index >= 0) ? shortUri.substring(0, index) : shortUri; - } + private void at(PageTuple page) { + // Is Universal? + synchronized (lock) { + String key = firstPathElement(page.getUri()); + if (isVariable(key)) { + universalMatchingPages.add(page); + } else { + multiput(pages, key, page); + } + } - @Nullable - public Page get(String uri) { - final String key = firstPathElement(uri); + // Actions are not backed by classes. + if (page.pageClass() != null) + classToPageMap.put(page.pageClass(), page); + } - List tuple = pages.get(key); + private PageTuple at(String uri, Class clazz, boolean headless) { + final String key = firstPathElement(uri); + final PageTuple pageTuple = new PageTuple(uri, new PathMatcherChain(uri), clazz, injector, headless, false); - //first try static first piece - if (null != tuple) { + synchronized (lock) { + //is universal? (i.e. first element is a variable) + if (isVariable(key)) + universalMatchingPages.add(pageTuple); + else { + multiput(pages, key, pageTuple); + } + } - //first try static first piece - for (PageTuple pageTuple : tuple) { - if (pageTuple.matcher.matches(uri)) - return pageTuple; - } - } + // Does not need to be inside lock, as it is concurrent. + classToPageMap.put(clazz, pageTuple); - //now try dynamic first piece (how can we make this faster?) - for (PageTuple pageTuple : universalMatchingPages) { - if (pageTuple.matcher.matches(uri)) return pageTuple; } - //nothing matched - return null; - } + @Override + public Page embedAs(Class clazz, String as) { + Preconditions.checkArgument(null == clazz.getAnnotation(Service.class), + "You cannot embed headless web services!"); + PageTuple pageTuple = new PageTuple("", PathMatcherChain.ignoring(), clazz, injector, false, false); - public Page forName(String name) { - return pagesByName.get(name); - } + synchronized (lock) { + pagesByName.put(as.toLowerCase(), pageTuple); + } - @Nullable - public Page forInstance(Object instance) { - Class aClass = instance.getClass(); - PageTuple targetType = classToPageMap.get(aClass); + return pageTuple; + } - // Do a super crawl to detect the target type. - while (null == targetType) { - aClass = aClass.getSuperclass(); - targetType = classToPageMap.get(aClass); + @Override + public Page decorate(Class pageClass) { + Preconditions.checkArgument(null == pageClass.getAnnotation(Service.class), + "You cannot extend headless web services!"); + PageTuple pageTuple = new PageTuple("", PathMatcherChain.ignoring(), pageClass, injector, false, true); + + // store page with a special name used by ExtendWidget + String name = DecorateWidget.embedNameFor(pageClass); + synchronized (lock) { + pagesByName.put(name, pageTuple); + } - // Stop at the root =D - if (Object.class.equals(aClass)) { - return null; - } + return pageTuple; } - return InstanceBoundPage.delegating(targetType, instance); - } + @Override + public Page nonCompilingGet(String uri) { + // The regular get is non compiling, in our case. So these methods are identical. + return get(uri); + } - public Page forClass(Class pageClass) { - return classToPageMap.get(pageClass); - } + private static void multiput(Map> pages, String key, PageTuple page) { + List list = pages.get(key); - public static class InstanceBoundPage implements Page { - private final Page delegate; - private final Object instance; + if (null == list) { + list = new ArrayList(); + pages.put(key, list); + } - private InstanceBoundPage(Page delegate, Object instance) { - this.delegate = delegate; - this.instance = instance; + list.add(page); } - public Renderable widget() { - return delegate.widget(); + private static boolean isVariable(String key) { + return key.length() > 0 && ':' == key.charAt(0); } - public Object instantiate() { - return instance; - } + String firstPathElement(String uri) { + String shortUri = uri.substring(1); - public Object doMethod(String httpMethod, Object page, String pathInfo, - HttpServletRequest request) { - return delegate.doMethod(httpMethod, page, pathInfo, request); - } + final int index = shortUri.indexOf("/"); - public Class pageClass() { - return delegate.pageClass(); - } - - public void apply(Renderable widget) { - delegate.apply(widget); + return (index >= 0) ? shortUri.substring(0, index) : shortUri; } - public String getUri() { - return delegate.getUri(); - } + @Override + @Nullable + public Page get(String uri) { + final String key = firstPathElement(uri); + + List tuple = pages.get(key); + + //first try static first piece + if (null != tuple) { + + //first try static first piece + for (PageTuple pageTuple : tuple) { + if (pageTuple.matcher.matches(uri)) + return pageTuple; + } + } - public boolean isHeadless() { - return delegate.isHeadless(); + //now try dynamic first piece (how can we make this faster?) + for (PageTuple pageTuple : universalMatchingPages) { + if (pageTuple.matcher.matches(uri)) + return pageTuple; + } + + //nothing matched + return null; } - + @Override - public boolean isDecorated() { - return delegate.isDecorated(); - } - - public Set getMethod() { - return delegate.getMethod(); + public Page forName(String name) { + return pagesByName.get(name); } - public int compareTo(Page page) { - return delegate.compareTo(page); - } + @Override + @Nullable + public Page forInstance(Object instance) { + Class aClass = instance.getClass(); + PageTuple targetType = classToPageMap.get(aClass); + + // Do a super crawl to detect the target type. + while (null == targetType) { + aClass = aClass.getSuperclass(); + targetType = classToPageMap.get(aClass); + + // Stop at the root =D + if (Object.class.equals(aClass)) { + return null; + } + } - public static InstanceBoundPage delegating(Page delegate, Object instance) { - return new InstanceBoundPage(delegate, instance); + return InstanceBoundPage.delegating(targetType, instance); } - } - - @Select("") //the default select (hacky!!) - public static class PageTuple implements Page { - private final String uri; - private final PathMatcher matcher; - private final AtomicReference pageWidget = new AtomicReference(); - private final Class clazz; - private final boolean headless; - private final boolean extension; - private final Injector injector; - private final Multimap methods; - - //dispatcher switch (select on request param by default) - private final Select select; - private static final Key>> HTTP_METHODS_KEY = - Key.get(new TypeLiteral>>() {}, Bricks.class); - - // A map of http methods -> annotation types (e.g. "POST" -> @Post) - private Map> httpMethods; - - public PageTuple(String uri, PathMatcher matcher, Class clazz, boolean headless, boolean extension, - Injector injector, Multimap methods) { - this.uri = uri; - this.matcher = matcher; - this.clazz = clazz; - this.headless = headless; - this.extension = extension; - this.injector = injector; - this.methods = methods; - this.select = PageTuple.class.getAnnotation(Select.class); - this.httpMethods = injector.getInstance(HTTP_METHODS_KEY); + @Override + public Page forClass(Class pageClass) { + return classToPageMap.get(pageClass); } - public PageTuple(String uri, PathMatcher matcher, Class clazz, Injector injector, - boolean headless, boolean extension) { - this.uri = uri; - this.matcher = matcher; - this.clazz = clazz; - this.injector = injector; - this.headless = headless; - this.extension = extension; + public static class InstanceBoundPage implements Page { + private final Page delegate; + private final Object instance; + + private InstanceBoundPage(Page delegate, Object instance) { + this.delegate = delegate; + this.instance = instance; + } - this.select = discoverSelect(clazz); + @Override + public Renderable widget() { + return delegate.widget(); + } - this.httpMethods = injector.getInstance(HTTP_METHODS_KEY); - this.methods = reflectAndCache(uri, httpMethods); - } + @Override + public Object instantiate() { + return instance; + } + + @Override + public Object doMethod(String httpMethod, Object page, String pathInfo, HttpServletRequest request) { + return delegate.doMethod(httpMethod, page, pathInfo, request); + } - //the @Select request parameter-based event dispatcher - private Select discoverSelect(Class clazz) { - final Select select = clazz.getAnnotation(Select.class); - if (null != select) - return select; - else - return PageTuple.class.getAnnotation(Select.class); + @Override + public Class pageClass() { + return delegate.pageClass(); + } + + @Override + public void apply(Renderable widget) { + delegate.apply(widget); + } + + @Override + public String getUri() { + return delegate.getUri(); + } + + @Override + public boolean isHeadless() { + return delegate.isHeadless(); + } + + @Override + public boolean isDecorated() { + return delegate.isDecorated(); + } + + @Override + public Set getMethod() { + return delegate.getMethod(); + } + + @Override + public int compareTo(Page page) { + return delegate.compareTo(page); + } + + public static InstanceBoundPage delegating(Page delegate, Object instance) { + return new InstanceBoundPage(delegate, instance); + } } - /** - * Returns a map of HTTP-method name to @Annotation-marked methods - */ - @SuppressWarnings({"JavaDoc"}) - private Multimap reflectAndCache(String uri, - Map> methodMap) { - String tail = ""; - if (clazz.isAnnotationPresent(At.class)) { - int length = clazz.getAnnotation(At.class).value().length(); - - // It's possible that the uri being registered is shorter than the - // class length, this can happen in the case of using the .at() module - // directive to override @At() URI path mapping. In this case we treat - // this call as a top-level path registration with no tail. Any - // encountered subpath @At methods will be ignored for this URI. - if (uri != null && length <= uri.length()) - tail = uri.substring(length); - } - - Multimap map = HashMultimap.create(); - - for (Map.Entry> entry : methodMap.entrySet()) { - - Class get = entry.getValue(); - // First search any available public methods and store them (including inherited ones) - for (Method method : clazz.getMethods()) { - if (method.isAnnotationPresent(get)) { - if (!method.isAccessible()) - method.setAccessible(true); //ugh - - // Be defensive about subpaths. - if (method.isAnnotationPresent(At.class)) { - // Skip any at-annotated methods for a top-level path registration. - if (tail.isEmpty()) { - continue; - } + @Select("") + //the default select (hacky!!) + public static class PageTuple implements Page { + private final String uri; + private final PathMatcher matcher; + private final AtomicReference pageWidget = new AtomicReference(); + private final Class clazz; + private final boolean headless; + private final boolean extension; + private final Injector injector; + + private final Multimap methods; + + //dispatcher switch (select on request param by default) + private final Select select; + private static final Key>> HTTP_METHODS_KEY = Key.get( + new TypeLiteral>>() { + }, Bricks.class); + + // A map of http methods -> annotation types (e.g. "POST" -> @Post) + private final Map> httpMethods; + + public PageTuple(String uri, PathMatcher matcher, Class clazz, boolean headless, boolean extension, + Injector injector, Multimap methods) { + this.uri = uri; + this.matcher = matcher; + this.clazz = clazz; + this.headless = headless; + this.extension = extension; + this.injector = injector; + this.methods = methods; + this.select = PageTuple.class.getAnnotation(Select.class); + this.httpMethods = injector.getInstance(HTTP_METHODS_KEY); + } - // Skip any at-annotated methods that do not exactly match the path. - if (!tail.equals(method.getAnnotation(At.class).value())) { - continue; - } - } else if (!tail.isEmpty()) { - // If this is the top-level method we're scanning, but their is a tail, i.e. - // this is not intended to be served by the top-level method, then skip. - continue; - } - - // Otherwise register this method for firing... - - //remember default value is empty string - String value = getValue(get, method); - String key = (Strings.empty(value)) ? entry.getKey() : entry.getKey() + value; - map.put(key, new MethodTuple(method, injector)); + public PageTuple(String uri, PathMatcher matcher, Class clazz, Injector injector, boolean headless, + boolean extension) { + this.uri = uri; + this.matcher = matcher; + this.clazz = clazz; + this.injector = injector; + this.headless = headless; + this.extension = extension; + + this.select = discoverSelect(clazz); + + this.httpMethods = injector.getInstance(HTTP_METHODS_KEY); + this.methods = reflectAndCache(uri, httpMethods); + } + + //the @Select request parameter-based event dispatcher + private Select discoverSelect(Class clazz) { + final Select select = clazz.getAnnotation(Select.class); + if (null != select) + return select; + else + return PageTuple.class.getAnnotation(Select.class); + } + + /** + * Returns a map of HTTP-method name to @Annotation-marked methods + */ + @SuppressWarnings({ "JavaDoc" }) + private Multimap reflectAndCache(String uri, Map> methodMap) { + String tail = ""; + if (clazz.isAnnotationPresent(At.class)) { + int length = clazz.getAnnotation(At.class).value().length(); + + // It's possible that the uri being registered is shorter than the + // class length, this can happen in the case of using the .at() module + // directive to override @At() URI path mapping. In this case we treat + // this call as a top-level path registration with no tail. Any + // encountered subpath @At methods will be ignored for this URI. + if (uri != null && length <= uri.length()) + tail = uri.substring(length); } - } - - // Then search class's declared methods only (these take precedence) - for (Method method : clazz.getDeclaredMethods()) { - if (method.isAnnotationPresent(get)) { - if (!method.isAccessible()) - method.setAccessible(true); //ugh - - // Be defensive about subpaths. - if (method.isAnnotationPresent(At.class)) { - // Skip any at-annotated methods for a top-level path registration. - if (tail.isEmpty()) { - continue; + + Multimap map = HashMultimap.create(); + + for (Map.Entry> entry : methodMap.entrySet()) { + + Class get = entry.getValue(); + // First search any available public methods and store them (including inherited ones) + for (Method method : clazz.getMethods()) { + if (method.isAnnotationPresent(get)) { + if (!method.isAccessible()) + method.setAccessible(true); //ugh + + // Be defensive about subpaths. + if (method.isAnnotationPresent(At.class)) { + // Skip any at-annotated methods for a top-level path registration. + if (tail.isEmpty()) { + continue; + } + + // Skip any at-annotated methods that do not exactly match the path. + if (!tail.equals(method.getAnnotation(At.class).value())) { + continue; + } + } else if (!tail.isEmpty()) { + // If this is the top-level method we're scanning, but their is a tail, i.e. + // this is not intended to be served by the top-level method, then skip. + continue; + } + + // Otherwise register this method for firing... + + //remember default value is empty string + String value = getValue(get, method); + String key = (Strings.empty(value)) ? entry.getKey() : entry.getKey() + value; + map.put(key, new MethodTuple(method, injector)); + } } - // Skip any at-annotated methods that do not exactly match the path. - if (!tail.equals(method.getAnnotation(At.class).value())) { - continue; + // Then search class's declared methods only (these take precedence) + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(get)) { + if (!method.isAccessible()) + method.setAccessible(true); //ugh + + // Be defensive about subpaths. + if (method.isAnnotationPresent(At.class)) { + // Skip any at-annotated methods for a top-level path registration. + if (tail.isEmpty()) { + continue; + } + + // Skip any at-annotated methods that do not exactly match the path. + if (!tail.equals(method.getAnnotation(At.class).value())) { + continue; + } + } else if (!tail.isEmpty()) { + // If this is the top-level method we're scanning, but their is a tail, i.e. + // this is not intended to be served by the top-level method, then skip. + continue; + } + + // Otherwise register this method for firing... + + //remember default value is empty string + String value = getValue(get, method); + String key = (Strings.empty(value)) ? entry.getKey() : entry.getKey() + value; + map.put(key, new MethodTuple(method, injector)); + } } - } else if (!tail.isEmpty()) { - // If this is the top-level method we're scanning, but their is a tail, i.e. - // this is not intended to be served by the top-level method, then skip. - continue; - } - - // Otherwise register this method for firing... - - //remember default value is empty string - String value = getValue(get, method); - String key = (Strings.empty(value)) ? entry.getKey() : entry.getKey() + value; - map.put(key, new MethodTuple(method, injector)); } - } - } - return map; - } + return map; + } - private String getValue(Class get, Method method) { - return readAnnotationValue(method.getAnnotation(get)); - } + private String getValue(Class get, Method method) { + return readAnnotationValue(method.getAnnotation(get)); + } - public Renderable widget() { - return pageWidget.get(); - } + @Override + public Renderable widget() { + return pageWidget.get(); + } - public Object instantiate() { - return clazz == null ? Collections.emptyMap() : injector.getInstance(clazz); - } + @Override + public Object instantiate() { + return clazz == null ? Collections.emptyMap() : injector.getInstance(clazz); + } - public boolean isHeadless() { - return headless; - } - - @Override - public boolean isDecorated() { - return extension; - } - - public Set getMethod() { - return methods.keySet(); - } + @Override + public boolean isHeadless() { + return headless; + } - public int compareTo(Page page) { - return uri.compareTo(page.getUri()); - } + @Override + public boolean isDecorated() { + return extension; + } - public Object doMethod(String httpMethod, Object page, String pathInfo, - HttpServletRequest request) { + @Override + public Set getMethod() { + return methods.keySet(); + } - //nothing to fire - if (Strings.empty(httpMethod)) { - return null; - } - - @SuppressWarnings("unchecked") // Guaranteed by javax.servlet - Map params = (Map) request.getParameterMap(); - - // Extract injectable pieces of the pathInfo. - final Map map = matcher.findMatches(pathInfo); - - //find method(s) to dispatch - // to - final String[] events = params.get(select.value()); - if (null != events) { - boolean matched = false; - for (String event : events) { - String key = httpMethod + event; - Collection tuples = methods.get(key); - Object redirect = null; - - if (null != tuples) { - for (Action action : tuples) { - if (action.shouldCall(request)) { - matched = true; - redirect = action.call(page, map); - break; - } + @Override + public int compareTo(Page page) { + return uri.compareTo(page.getUri()); + } + + @Override + public Object doMethod(String httpMethod, Object page, String pathInfo, HttpServletRequest request) { + + //nothing to fire + if (Strings.empty(httpMethod)) { + return null; + } + + @SuppressWarnings("unchecked") + // Guaranteed by javax.servlet + Map params = request.getParameterMap(); + + // Extract injectable pieces of the pathInfo. + final Map map = matcher.findMatches(pathInfo); + + //find method(s) to dispatch + // to + final String[] events = params.get(select.value()); + if (null != events) { + boolean matched = false; + for (String event : events) { + String key = httpMethod + event; + Collection tuples = methods.get(key); + Object redirect = null; + + if (null != tuples) { + for (Action action : tuples) { + if (action.shouldCall(request)) { + matched = true; + redirect = action.call(page, map); + break; + } + } + } + + //redirects interrupt the event dispatch sequence. Note this might cause inconsistent behaviour depending on + // the order of processing for events. + if (null != redirect) { + return redirect; + } + } + + // no matched events. Fire default handler + if (!matched) { + return callAction(httpMethod, page, map, request); + } + + } else { + // Fire default handler (no events defined) + return callAction(httpMethod, page, map, request); } - } - //redirects interrupt the event dispatch sequence. Note this might cause inconsistent behaviour depending on - // the order of processing for events. - if (null != redirect) { + //no redirects, render normally + return null; + } + + private Object callAction(String httpMethod, Object page, Map pathMap, + HttpServletRequest request) { + + // There may be more than one default handler + Collection tuple = methods.get(httpMethod); + Object redirect = null; + if (null != tuple) { + for (Action action : tuple) { + if (action.shouldCall(request)) { + redirect = action.call(page, pathMap); + break; + } + } + } return redirect; - } + } - // no matched events. Fire default handler - if (!matched) { - return callAction(httpMethod, page, map, request); + @Override + public Class pageClass() { + return clazz; } - } else { - // Fire default handler (no events defined) - return callAction(httpMethod, page, map, request); - } + @Override + public void apply(Renderable widget) { + this.pageWidget.set(widget); + } - //no redirects, render normally - return null; - } + @Override + public String getUri() { + return uri; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof Page)) + return false; - private Object callAction(String httpMethod, Object page, Map pathMap, - HttpServletRequest request) { + Page that = (Page) o; - // There may be more than one default handler - Collection tuple = methods.get(httpMethod); - Object redirect = null; - if (null != tuple) { - for (Action action : tuple) { - if (action.shouldCall(request)) { - redirect = action.call(page, pathMap); - break; - } + return this.clazz.equals(that.pageClass()) && isDecorated() == that.isDecorated(); } - } - return redirect; - } + @Override + public int hashCode() { + return clazz.hashCode(); + } - public Class pageClass() { - return clazz; - } - - public void apply(Renderable widget) { - this.pageWidget.set(widget); + @Override + public String toString() { + return Objects.toStringHelper(PageTuple.class).add("clazz", clazz).add("isDecorated", extension) + .add("uri", uri).toString(); + } } - public String getUri() { - return uri; - } + private static class MethodTuple implements Action { + private final Method method; + private final Injector injector; + private final List args; + private final Map negotiates; + private final ContentNegotiator negotiator; + private final TypeConverter converter; + + private MethodTuple(Method method, Injector injector) { + this.method = method; + this.injector = injector; + this.args = reflect(method); + this.negotiates = discoverNegotiates(method, injector); + this.negotiator = injector.getInstance(ContentNegotiator.class); + this.converter = injector.getInstance(TypeConverter.class); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Page)) return false; + private List reflect(Method method) { + final Annotation[][] annotationsGrid = method.getParameterAnnotations(); + if (null == annotationsGrid) + return Collections.emptyList(); - Page that = (Page) o; + List args = new ArrayList(); + for (int i = 0; i < annotationsGrid.length; i++) { + Annotation[] annotations = annotationsGrid[i]; - return this.clazz.equals(that.pageClass()); - } + Annotation bindingAnnotation = null; + boolean namedFound = false; + for (Annotation annotation : annotations) { + if (Named.class.isInstance(annotation)) { + Named named = (Named) annotation; - @Override - public int hashCode() { - return clazz.hashCode(); - } - } + args.add(new NamedParameter(named.value(), method.getGenericParameterTypes()[i])); + namedFound = true; - private static class MethodTuple implements Action { - private final Method method; - private final Injector injector; - private final List args; - private final Map negotiates; - private final ContentNegotiator negotiator; - private final TypeConverter converter; - - private MethodTuple(Method method, Injector injector) { - this.method = method; - this.injector = injector; - this.args = reflect(method); - this.negotiates = discoverNegotiates(method, injector); - this.negotiator = injector.getInstance(ContentNegotiator.class); - this.converter = injector.getInstance(TypeConverter.class); - } + break; + } else if (annotation.annotationType().isAnnotationPresent(BindingAnnotation.class)) { + bindingAnnotation = annotation; + } + } - private List reflect(Method method) { - final Annotation[][] annotationsGrid = method.getParameterAnnotations(); - if (null == annotationsGrid) - return Collections.emptyList(); + if (!namedFound) { + // Could be an arbitrary injection request. + Class argType = method.getParameterTypes()[i]; + Key key = (null != bindingAnnotation) ? Key.get(argType, bindingAnnotation) : Key.get(argType); - List args = new ArrayList(); - for (int i = 0; i < annotationsGrid.length; i++) { - Annotation[] annotations = annotationsGrid[i]; + args.add(key); - Annotation bindingAnnotation = null; - boolean namedFound = false; - for (Annotation annotation : annotations) { - if (Named.class.isInstance(annotation)) { - Named named = (Named) annotation; + if (null == injector.getBindings().get(key)) + throw new InvalidEventHandlerException( + "Encountered an argument not annotated with @Named and not a valid injection key" + + " in event handler method: " + method + " " + key); + } - args.add(new NamedParameter(named.value(), method.getGenericParameterTypes()[i])); - namedFound = true; + } - break; - } else if (annotation.annotationType().isAnnotationPresent(BindingAnnotation.class)) { - bindingAnnotation = annotation; - } + return Collections.unmodifiableList(args); } - if (!namedFound) { - // Could be an arbitrary injection request. - Class argType = method.getParameterTypes()[i]; - Key key = (null != bindingAnnotation) - ? Key.get(argType, bindingAnnotation) - : Key.get(argType); + /** + * @return true if this method tuple can be validly called against this request. + * Used to select for content negotiation. + */ + @Override + public boolean shouldCall(HttpServletRequest request) { + return negotiator.shouldCall(negotiates, request); + } - args.add(key); + @Override + public Object call(Object page, Map map) { + List arguments = new ArrayList(); + for (Object arg : args) { + if (arg instanceof NamedParameter) { + NamedParameter np = (NamedParameter) arg; + String text = map.get(np.getName()); + Object value = converter.convert(text, np.getType()); + arguments.add(value); + } else + arguments.add(injector.getInstance((Key) arg)); + } - if (null == injector.getBindings().get(key)) - throw new InvalidEventHandlerException( - "Encountered an argument not annotated with @Named and not a valid injection key" - + " in event handler method: " + method + " " + key); + return call(page, method, arguments.toArray()); } - } + private static Object call(Object page, final Method method, Object[] args) { + try { + return method.invoke(page, args); + } catch (IllegalAccessException e) { + throw new EventDispatchException("Could not access event method (appears to be a security problem): " + + method, e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + StackTraceElement[] stackTrace = cause.getStackTrace(); + throw new EventDispatchException(String.format( + "Exception [%s - \"%s\"] thrown by event method [%s]\n\nat %s\n" + + "(See below for entire trace.)\n", cause.getClass().getSimpleName(), + cause.getMessage(), method, stackTrace[0]), e); + } + } - return Collections.unmodifiableList(args); - } + //the @Accept request header-based event dispatcher + private Map discoverNegotiates(Method method, Injector injector) { + // This ugly gunk gets us the map of headers to negotiation annotations + Map> negotiationsMap = injector.getInstance(Key.get( + new TypeLiteral>>() { + }, Negotiation.class)); + + Map negotiations = Maps.newHashMap(); + // Gather all the negotiation annotations in this class. + for (Map.Entry> headerAnn : negotiationsMap.entrySet()) { + Annotation annotation = method.getAnnotation(headerAnn.getValue()); + if (annotation != null) { + negotiations.put(headerAnn.getKey(), readAnnotationValue(annotation)); + } + } - /** - * @return true if this method tuple can be validly called against this request. - * Used to select for content negotiation. - */ - @Override - public boolean shouldCall(HttpServletRequest request) { - return negotiator.shouldCall(negotiates, request); - } + return negotiations; + } - - @Override - public Object call(Object page, Map map) { - List arguments = new ArrayList(); - for (Object arg : args) { - if (arg instanceof NamedParameter) { - NamedParameter np = (NamedParameter) arg; - String text = map.get(np.getName()); - Object value = converter.convert(text, np.getType()); - arguments.add(value); - } else - arguments.add(injector.getInstance((Key) arg)); - } - - return call(page, method, arguments.toArray()); - } + public class NamedParameter { + private final String name; + private final Type type; - private static Object call(Object page, final Method method, - Object[] args) { - try { - return method.invoke(page, args); - } catch (IllegalAccessException e) { - throw new EventDispatchException( - "Could not access event method (appears to be a security problem): " + method, e); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - StackTraceElement[] stackTrace = cause.getStackTrace(); - throw new EventDispatchException(String.format( - "Exception [%s - \"%s\"] thrown by event method [%s]\n\nat %s\n" - + "(See below for entire trace.)\n", - cause.getClass().getSimpleName(), - cause.getMessage(), method, - stackTrace[0]), e); - } - } + public NamedParameter(String name, Type type) { + this.name = name; + this.type = type; + } - //the @Accept request header-based event dispatcher - private Map discoverNegotiates(Method method, Injector injector) { - // This ugly gunk gets us the map of headers to negotiation annotations - Map> negotiationsMap = injector.getInstance( - Key.get(new TypeLiteral>>(){ }, Negotiation.class)); + public String getName() { + return name; + } - Map negotiations = Maps.newHashMap(); - // Gather all the negotiation annotations in this class. - for (Map.Entry> headerAnn : negotiationsMap.entrySet()) { - Annotation annotation = method.getAnnotation(headerAnn.getValue()); - if (annotation != null) { - negotiations.put(headerAnn.getKey(), readAnnotationValue(annotation)); + public Type getType() { + return type; + } } - } - return negotiations; } - - public class NamedParameter { - private final String name; - private final Type type; - - public NamedParameter(String name, Type type) { - this.name = name; - this.type = type; - } - - public String getName() { - return name; - } - - public Type getType() { - return type; - } - } - - } - - /** - * A simple utility method that reads the String value attribute of any annotation - * instance. - */ - static String readAnnotationValue(Annotation annotation) { - try { - Method m = annotation.getClass().getMethod("value"); - - return (String) m.invoke(annotation); - - } catch (NoSuchMethodException e) { - throw new IllegalStateException("Encountered a configured annotation that " + - "has no value parameter. This should never happen. " + annotation, e); - } catch (InvocationTargetException e) { - throw new IllegalStateException("Encountered a configured annotation that " + - "could not be read." + annotation, e); - } catch (IllegalAccessException e) { - throw new IllegalStateException("Encountered a configured annotation that " + - "could not be read." + annotation, e); - } - } + + /** + * A simple utility method that reads the String value attribute of any annotation + * instance. + */ + static String readAnnotationValue(Annotation annotation) { + try { + Method m = annotation.getClass().getMethod("value"); + + return (String) m.invoke(annotation); + + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Encountered a configured annotation that " + + "has no value parameter. This should never happen. " + annotation, e); + } catch (InvocationTargetException e) { + throw new IllegalStateException("Encountered a configured annotation that " + "could not be read." + + annotation, e); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Encountered a configured annotation that " + "could not be read." + + annotation, e); + } + } } diff --git a/slf4j/pom.xml b/slf4j/pom.xml index fd9ecbf6..9b39f914 100644 --- a/slf4j/pom.xml +++ b/slf4j/pom.xml @@ -45,11 +45,11 @@ ext-snapshot-local - http://cerberus.local:8080/artifactory/ext-snapshot-local + http://nightflight.local:8383/artifactory/ext-snapshot-local ext-release-local - http://cerberus.local:8080/artifactory/ext-release-local + http://nightflight.local:8383/artifactory/ext-release-local diff --git a/stat/pom.xml b/stat/pom.xml index fd715b1f..78344701 100644 --- a/stat/pom.xml +++ b/stat/pom.xml @@ -77,11 +77,11 @@ ext-snapshot-local - http://cerberus.local:8080/artifactory/ext-snapshot-local + http://nightflight.local:8383/artifactory/ext-snapshot-local ext-release-local - http://cerberus.local:8080/artifactory/ext-release-local + http://nightflight.local:8383/artifactory/ext-release-local From fc79934edd95e30342667fa5aaddd99a5a45c3b7 Mon Sep 17 00:00:00 2001 From: james Date: Mon, 30 Jan 2012 13:12:32 -0600 Subject: [PATCH 10/20] lost this edit when I pulled the pennyclickui over-rides into this project. I know I had this in here before --- .../rendering/control/DecorateWidget.java | 98 ++++++++++--------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/DecorateWidget.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/DecorateWidget.java index f8ca75f1..cad2c870 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/DecorateWidget.java +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/DecorateWidget.java @@ -13,56 +13,65 @@ /** * @author John Patterson (jdpatterson@gmail.com) - * + * */ public class DecorateWidget implements Renderable { - @Inject private PageBook book; - - private ThreadLocal> templateClassLocal = new ThreadLocal>(); - + @Inject + private PageBook book; + + private final ThreadLocal> templateClassLocal = new ThreadLocal>(); + public static String embedNameFor(Class pageClass) { - return pageClass.getName().toLowerCase() + "-extend"; + String lowerCaseClassName = pageClass.getName().toLowerCase(); + int indexOf = lowerCaseClassName.indexOf("$$enhancerbyguice"); + if (indexOf > 0) { + lowerCaseClassName = lowerCaseClassName.substring(0, indexOf); + } + return lowerCaseClassName + "-extend"; } - - public DecorateWidget(WidgetChain chain, String expression, Evaluator evaluator){ + + public DecorateWidget(WidgetChain chain, String expression, + Evaluator evaluator) { // do not need any of the compulsory constructor args } - + @Override public void render(Object bound, Respond respond) { Class templateClass; Class previousTemplateClass = templateClassLocal.get(); try { - if (previousTemplateClass == null) { - templateClass = bound.getClass(); - } - else { - // get the extension subclass above the last - templateClass = nextExtensionSubclass(previousTemplateClass, bound.getClass()); - if (templateClass == null) { - throw new IllegalStateException("Could not find subclass of " + previousTemplateClass.getName() + " with @Extension annotation."); - } - } - templateClassLocal.set(templateClass); - - // get the extension page by name - PageBook.Page page = book.forName(DecorateWidget.embedNameFor(templateClass)); - - // create a dummy respond to collect the output of the embedded page - StringBuilderRespond sbrespond = new StringBuilderRespond(); - EmbeddedRespond embedded = new EmbeddedRespond(null, sbrespond); - page.widget().render(bound, embedded); - - // write the head and content to the real respond - respond.writeToHead(embedded.toHeadString()); - respond.write(embedded.toString()); - - // free some memory - embedded.clear(); - } - finally { + if (previousTemplateClass == null) { + templateClass = bound.getClass(); + } else { + // get the extension subclass above the last + templateClass = nextExtensionSubclass(previousTemplateClass, + bound.getClass()); + if (templateClass == null) { + throw new IllegalStateException("Could not find subclass of " + + previousTemplateClass.getName() + + " with @Extension annotation."); + } + } + templateClassLocal.set(templateClass); + + // get the extension page by name + PageBook.Page page = book.forName(DecorateWidget + .embedNameFor(templateClass)); + + // create a dummy respond to collect the output of the embedded page + StringBuilderRespond sbrespond = new StringBuilderRespond(); + EmbeddedRespond embedded = new EmbeddedRespond(null, sbrespond); + page.widget().render(bound, embedded); + + // write the head and content to the real respond + respond.writeToHead(embedded.toHeadString()); + respond.write(embedded.toString()); + + // free some memory + embedded.clear(); + } finally { // we are finished with this extension if (previousTemplateClass == null) { templateClassLocal.set(null); @@ -71,23 +80,22 @@ public void render(Object bound, Respond respond) { } // recursively find the next subclass with an @Extension annotation - private Class nextExtensionSubclass(Class previousTemplagteClass, Class candidate) { + private Class nextExtensionSubclass(Class previousTemplagteClass, + Class candidate) { if (candidate == previousTemplagteClass) { // terminate the recursion return null; - } - else if (candidate == Object.class) { + } else if (candidate == Object.class) { // this should never happen - we should terminate recursion first throw new IllegalStateException("Did not find previsou extension"); - } - else { + } else { // check the super class for the result - Class result = nextExtensionSubclass(previousTemplagteClass, candidate.getSuperclass()); + Class result = nextExtensionSubclass(previousTemplagteClass, + candidate.getSuperclass()); if (result == null && candidate.isAnnotationPresent(Decorated.class)) { // this is the one - retreat! return candidate; - } - else { + } else { // we still have not found one return null; } From 9a0a1d3f1f096c5f5db5d30d0a8fa9ca200fcc69 Mon Sep 17 00:00:00 2001 From: james Date: Tue, 7 Feb 2012 22:28:31 -0600 Subject: [PATCH 11/20] sometimes pages want to be HEADLESS (this solution may be causing a second render) --- .../routing/WidgetRoutingDispatcher.java | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java index 1ed1b4e8..47c470c7 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java @@ -31,9 +31,8 @@ class WidgetRoutingDispatcher implements RoutingDispatcher { @Inject public WidgetRoutingDispatcher(PageBook book, RequestBinder binder, - ResourcesService resourcesService, - Provider flashCacheProvider, - HeadlessRenderer headlessRenderer) { + ResourcesService resourcesService, + Provider flashCacheProvider, HeadlessRenderer headlessRenderer) { this.headlessRenderer = headlessRenderer; this.book = book; this.binder = binder; @@ -44,7 +43,7 @@ public WidgetRoutingDispatcher(PageBook book, RequestBinder binder, public Object dispatch(Request request, Events event) throws IOException { String uri = request.path(); - //first try dispatching as a static resource service + // first try dispatching as a static resource service Respond respond = resourcesService.serve(uri); if (null != respond) @@ -64,7 +63,7 @@ public Object dispatch(Request request, Events event) throws IOException { if (null == page) page = book.get(uri); - //could not dispatch as there was no match + // could not dispatch as there was no match if (null == page) return null; @@ -72,14 +71,14 @@ public Object dispatch(Request request, Events event) throws IOException { if (page.isHeadless()) { return bindAndReply(request, page, instance); } - - //fire events and render reponders - return bindAndRespond(request, page, instance); - + + // fire events and render reponders + return bindAndRespond(request, page, instance, uri); } - private Object bindAndReply(Request request, Page page, Object instance) throws IOException { + private Object bindAndReply(Request request, Page page, Object instance) + throws IOException { // bind request (sets request params, etc). binder.bind(request, instance); @@ -88,15 +87,15 @@ private Object bindAndReply(Request request, Page page, Object instance) throws } private Object bindAndRespond(Request request, PageBook.Page page, - Object instance) throws IOException { + Object instance, String uri) throws IOException { Respond respond = new StringBuilderRespond(instance); - //bind request + // bind request binder.bind(request, instance); - //fire get/post events + // fire get/post events final Object redirect = fireEvent(request, page, instance); - //render to respond + // render to respond if (null != redirect) { if (redirect instanceof String) @@ -107,18 +106,23 @@ else if (redirect instanceof Class) { // should never be null coz it is validated on compile. respond.redirect(contextualize(request, targetPage.getUri())); } else if (redirect instanceof Reply) { - //page wants to be headless + // page wants to be headless return bindAndReply(request, page, instance); } else { // Handle page-chaining driven redirection. PageBook.Page targetPage = book.forInstance(redirect); + String redirectUri = targetPage.getUri(); + if (redirect == instance) { + redirectUri = uri; + targetPage = page; + } // should never be null coz it will be validated at compile time. - flashCacheProvider.get().put(targetPage.getUri(), targetPage); + flashCacheProvider.get().put(redirectUri, targetPage); // Send to the canonical address of the page. This is also // verified at compile, not be a variablized matcher. - respond.redirect(contextualize(request, targetPage.getUri())); + respond.redirect(contextualize(request, redirectUri)); } } else { page.widget().render(instance, respond); From 858d3a361af6dd72b3e6184c55dd46241b0b04d7 Mon Sep 17 00:00:00 2001 From: james Date: Wed, 8 Feb 2012 18:37:30 -0600 Subject: [PATCH 12/20] velocity tools in the context --- .../com/google/sitebricks/example/SitebricksConfig.java | 3 +++ sitebricks/pom.xml | 5 +++++ .../com/google/sitebricks/compiler/StandardCompilers.java | 8 ++++++-- .../compiler/template/VelocityEngineProvider.java | 3 ++- .../compiler/template/VelocityTemplateCompiler.java | 6 ++++-- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java index e77bd17f..527c8e8e 100644 --- a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java @@ -23,6 +23,8 @@ import java.util.Locale; import java.util.Map; +import org.apache.velocity.runtime.directive.VelocimacroProxy; + /** * @author Dhanji R. Prasanna (dhanji@gmail.com) */ @@ -110,6 +112,7 @@ private void bindExplicitly() { // templating by extension at("/template").show(DecoratedPage.class); + at("/velocitySample").show(VelocitySample.class); embed(HelloWorld.class).as("Hello"); } diff --git a/sitebricks/pom.xml b/sitebricks/pom.xml index ad5ab4de..8002734d 100644 --- a/sitebricks/pom.xml +++ b/sitebricks/pom.xml @@ -21,6 +21,11 @@ velocity 1.7 + + org.apache.velocity + velocity-tools + 2.0 + oro oro diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java index e91bcb0b..db0e1aeb 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/StandardCompilers.java @@ -13,6 +13,7 @@ import com.google.sitebricks.Template; import com.google.sitebricks.TemplateLoader; import com.google.sitebricks.compiler.template.MvelTemplateCompiler; +import com.google.sitebricks.compiler.template.VelocityContextProvider; import com.google.sitebricks.compiler.template.VelocityEngineProvider; import com.google.sitebricks.compiler.template.VelocityTemplateCompiler; import com.google.sitebricks.compiler.template.freemarker.FreemarkerTemplateCompiler; @@ -35,16 +36,19 @@ class StandardCompilers implements Compilers { private final Map> httpMethods; private final TemplateLoader loader; private final VelocityEngineProvider velocityEngineProvider; +private final VelocityContextProvider velocityContextProvider; @Inject public StandardCompilers(WidgetRegistry registry, PageBook pageBook, SystemMetrics metrics, - @Bricks Map> httpMethods, TemplateLoader loader, VelocityEngineProvider velocityEngineProvider) { + @Bricks Map> httpMethods, TemplateLoader loader, VelocityEngineProvider velocityEngineProvider, + VelocityContextProvider velocityContextProvider) { this.registry = registry; this.pageBook = pageBook; this.metrics = metrics; this.httpMethods = httpMethods; this.loader = loader; this.velocityEngineProvider = velocityEngineProvider; + this.velocityContextProvider = velocityContextProvider; } public Renderable compileXml(Class page, String template) { @@ -74,7 +78,7 @@ public Renderable compileFreemarker( Class page, String template ) { @Override public Renderable compileVelocity(Class page, String template) { - return new VelocityTemplateCompiler(velocityEngineProvider).compile(template); + return new VelocityTemplateCompiler(velocityEngineProvider, velocityContextProvider).compile(template); } // TODO(dhanji): Feedback errors as return rather than throwing. diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityEngineProvider.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityEngineProvider.java index 28d947d0..270339a7 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityEngineProvider.java +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityEngineProvider.java @@ -25,7 +25,8 @@ public VelocityEngine get() { throw new RuntimeException(e); } - return new VelocityEngine(properties); + VelocityEngine velocityEngine = new VelocityEngine(properties); + return velocityEngine; } } diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java index f7593f5d..4278d737 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityTemplateCompiler.java @@ -14,10 +14,12 @@ public class VelocityTemplateCompiler { private final VelocityEngineProvider provider; + private final VelocityContextProvider velocityContextProvider; @Inject - public VelocityTemplateCompiler(VelocityEngineProvider provider) { + public VelocityTemplateCompiler(VelocityEngineProvider provider, VelocityContextProvider velocityContextProvider) { this.provider = provider; + this.velocityContextProvider = velocityContextProvider; } public Renderable compile(final String templateContent) { @@ -26,7 +28,7 @@ public Renderable compile(final String templateContent) { @Override public void render(Object bound, Respond respond) { - final VelocityContext context = new VelocityContext(); + VelocityContext context = velocityContextProvider.get(); context.put("page", bound); StringWriter writer = new StringWriter(); provider.get().evaluate(context, writer, "", templateContent); From e44f229c8b64a65781d029ccd61dc3be90e20ade Mon Sep 17 00:00:00 2001 From: james Date: Wed, 8 Feb 2012 18:38:09 -0600 Subject: [PATCH 13/20] velocity tools in the context --- .../template/VelocityContextProvider.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityContextProvider.java diff --git a/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityContextProvider.java b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityContextProvider.java new file mode 100644 index 00000000..63a0000a --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/compiler/template/VelocityContextProvider.java @@ -0,0 +1,22 @@ +package com.google.sitebricks.compiler.template; + +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.apache.velocity.VelocityContext; +import org.apache.velocity.tools.ToolManager; + +public class VelocityContextProvider implements Provider { + + @Override + @Singleton + public VelocityContext get() { + try { + ToolManager velocityToolManager = new ToolManager(); + velocityToolManager.configure("velocity-tools.xml"); + return new VelocityContext(velocityToolManager.createContext()); + } catch (RuntimeException e) { + return new VelocityContext(); + } + } +} From bcb37fb33166ee85ac13bade10b493ae901ddb37 Mon Sep 17 00:00:00 2001 From: james Date: Thu, 9 Feb 2012 07:54:30 -0600 Subject: [PATCH 14/20] property-ize snapshotRepository of distributionManagement for slf4j (somehow missed this earlier) --- slf4j/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/slf4j/pom.xml b/slf4j/pom.xml index 54345fd9..c8deb8d4 100644 --- a/slf4j/pom.xml +++ b/slf4j/pom.xml @@ -44,9 +44,9 @@ - google-snapshots - Sonatype OSS Nexus Snapshots - https://oss.sonatype.org/content/repositories/google-snapshots + ${snapshots.id} + ${snapshots.name} + ${snapshots.url} google-with-staging From dbf4653fdd027044b4dbb705c7ffd292d7075b69 Mon Sep 17 00:00:00 2001 From: james Date: Tue, 14 Feb 2012 16:17:00 -0600 Subject: [PATCH 15/20] When running Guice State.PRODUCTION it was discovered that the pagesToCompile Set was not compiling the @Decorated pages because the DefaultPageBook.PageTuple.equals method only looked at the class. This change incorporates the isDecorated boolean http://groups.google.com/group/google-sitebricks/browse_thread/thread/4ca57a97a8a31246 --- .../ScanAndCompileBootstrapper.java | 9 ++-- .../sitebricks/routing/DefaultPageBook.java | 44 ++++++++++++------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java b/sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java index 50a1a41d..2a5aab5e 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java +++ b/sitebricks/src/main/java/com/google/sitebricks/ScanAndCompileBootstrapper.java @@ -115,12 +115,14 @@ public void start() { } private void extendedPages(Set pagesToCompile) { + Set pageExtensions = Sets.newHashSet(); for (Page page : pagesToCompile) { if (page.pageClass().isAnnotationPresent(Decorated.class)) { // recursively add extension pages - analyseExtension(pagesToCompile, page.pageClass()); + analyseExtension(pageExtensions, page.pageClass()); } } + pagesToCompile.addAll(pageExtensions); } //processes all explicit bindings, including static resources. @@ -206,8 +208,9 @@ private Set scanPagesToCompile(Set> set) { return pagesToCompile; } - private void analyseExtension(Set pagesToCompile, Class extendClass) { + private void analyseExtension(Set pagesToCompile, final Class extendClassArgument) { // store the page with a special page name used by ExtendWidget + Class extendClass = extendClassArgument; pagesToCompile.add(pageBook.decorate(extendClass)); // recursively analyse super class @@ -221,7 +224,7 @@ else if (extendClass.isAnnotationPresent(Show.class)) { return; } } - throw new IllegalStateException("Could not find super class annotated with @Show"); + throw new IllegalStateException("Could not find super class annotated with @Show on parent of class: " + extendClassArgument); } private void compilePages(Set pagesToCompile) { diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java b/sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java index f3199fd5..17c3d928 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/DefaultPageBook.java @@ -1,5 +1,24 @@ package com.google.sitebricks.routing; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReference; + +import net.jcip.annotations.GuardedBy; +import net.jcip.annotations.ThreadSafe; + +import org.jetbrains.annotations.Nullable; + +import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; @@ -25,22 +44,6 @@ import com.google.sitebricks.http.negotiate.Negotiation; import com.google.sitebricks.rendering.Strings; import com.google.sitebricks.rendering.control.DecorateWidget; -import net.jcip.annotations.GuardedBy; -import net.jcip.annotations.ThreadSafe; -import org.jetbrains.annotations.Nullable; - -import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicReference; /** * contains active uri/widget mappings @@ -597,13 +600,20 @@ public boolean equals(Object o) { Page that = (Page) o; - return this.clazz.equals(that.pageClass()); + return this.clazz.equals(that.pageClass()) && isDecorated() == that.isDecorated(); } @Override public int hashCode() { return clazz.hashCode(); } + + @Override + public String toString() { + return Objects.toStringHelper(PageTuple.class).add("clazz", clazz).add("isDecorated", extension) + .add("uri", uri).toString(); + } + } private static class MethodTuple implements Action { From 6b85ae4fa719f57feea52c5c839eff132c749ea0 Mon Sep 17 00:00:00 2001 From: james Date: Tue, 14 Feb 2012 16:21:09 -0600 Subject: [PATCH 16/20] memory sharing issue in how (1) There is only one EmbedWidget per PageTuple, (2) that EmbedWidget has only one EmbeddedRespondFactory and (3) the EmbeddedRespondFactory was keeping a final StringBuilderRespond object to pass as the delegate to the factoried EmbeddedRespond. So, if two requests come in for the same page, they end up sharing a StringBuilderRespond --- .../sitebricks/rendering/control/EmbeddedRespondFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespondFactory.java b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespondFactory.java index 74857d2d..cc010649 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespondFactory.java +++ b/sitebricks/src/main/java/com/google/sitebricks/rendering/control/EmbeddedRespondFactory.java @@ -10,9 +10,9 @@ * @author Dhanji R. Prasanna (dhanji@gmail com) */ @Immutable class EmbeddedRespondFactory { - private final Respond respond = new StringBuilderRespond(new Object()); public EmbeddedRespond get(Map arguments) { + Respond respond = new StringBuilderRespond(new Object()); return new EmbeddedRespond(arguments, respond); } } From 40c5b9be897b2464359cf6db6c61ba9848ee74d6 Mon Sep 17 00:00:00 2001 From: james Date: Thu, 16 Feb 2012 12:00:05 -0600 Subject: [PATCH 17/20] add source jar for sitebricks (main project) --- sitebricks/pom.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sitebricks/pom.xml b/sitebricks/pom.xml index 8002734d..9aee8ed5 100644 --- a/sitebricks/pom.xml +++ b/sitebricks/pom.xml @@ -148,6 +148,23 @@ + + + + + org.apache.maven.plugins + maven-source-plugin + + + + jar + + + + + + + From 6a93b9b80ead7ccc850394ed3ddf4798803cd5b2 Mon Sep 17 00:00:00 2001 From: james Date: Tue, 21 Feb 2012 14:38:41 -0600 Subject: [PATCH 18/20] dorked up the merge when I brought the headless support over to the fork from the private modifications, when a page returns a Reply, it is just returned from dispatch --- .../com/google/sitebricks/routing/WidgetRoutingDispatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java index 47c470c7..3c86ed15 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java @@ -107,7 +107,7 @@ else if (redirect instanceof Class) { respond.redirect(contextualize(request, targetPage.getUri())); } else if (redirect instanceof Reply) { // page wants to be headless - return bindAndReply(request, page, instance); + return redirect; } else { // Handle page-chaining driven redirection. PageBook.Page targetPage = book.forInstance(redirect); From 49212890873298cdf64ec4e6477c1b21027fc1b5 Mon Sep 17 00:00:00 2001 From: james Date: Fri, 6 Apr 2012 13:20:19 -0500 Subject: [PATCH 19/20] testing teh velocity stuff and updating some converter tests --- .../com/google/sitebricks/example/Custom.java | 19 ++++++++++ .../sitebricks/example/CustomConverter.java | 36 +++++++++++++++++++ .../example/CustomToStringConverter.java | 17 +++++++++ .../sitebricks/example/SitebricksConfig.java | 2 ++ .../src/main/resources/CustomConverter.html | 16 +++++++++ .../CustomConverterAcceptanceTest.java | 30 ++++++++++++++++ .../acceptance/page/CustomConversionPage.java | 22 ++++++++++++ .../acceptance/page/VelocitySamplePage.java | 2 +- 8 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Custom.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CustomConverter.java create mode 100644 sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CustomToStringConverter.java create mode 100644 sitebricks-acceptance-tests/src/main/resources/CustomConverter.html create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/CustomConverterAcceptanceTest.java create mode 100644 sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/CustomConversionPage.java diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Custom.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Custom.java new file mode 100644 index 00000000..99621635 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/Custom.java @@ -0,0 +1,19 @@ +package com.google.sitebricks.example; + +public class Custom { + + private String encap; + + public Custom() { + encap = "encapsulated valuev"; + } + + public Custom(String value) { + encap = value; + } + + @Override + public String toString() { + return encap.toString(); + } +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CustomConverter.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CustomConverter.java new file mode 100644 index 00000000..f9345862 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CustomConverter.java @@ -0,0 +1,36 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.At; +import com.google.sitebricks.http.Get; +import com.google.sitebricks.http.Post; + +@At("/customConvertion") +public class CustomConverter { + + public static final String INITIAL_VALUE = "initial value"; + private Custom custom; + + public CustomConverter() { + custom = new Custom(INITIAL_VALUE); + } + + @Get + public void get() { + System.out.println("custom converter sample. woot!"); + } + + @Post + public void post() { + System.out.println("posted custom value: " + custom); + } + + public Custom getTestValue() { + return custom; + } + + public void setTestValue(Custom value) { + System.out.println("*************************** " + value); + custom = value; + } + +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CustomToStringConverter.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CustomToStringConverter.java new file mode 100644 index 00000000..8c3de169 --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/CustomToStringConverter.java @@ -0,0 +1,17 @@ +package com.google.sitebricks.example; + +import com.google.sitebricks.conversion.Converter; + +public class CustomToStringConverter implements Converter { + + @Override + public Custom to(String source) { + return new Custom(source); + } + + @Override + public String from(Custom target) { + return target.toString(); + } + +} diff --git a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java index 527c8e8e..04f6840f 100644 --- a/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java +++ b/sitebricks-acceptance-tests/src/main/java/com/google/sitebricks/example/SitebricksConfig.java @@ -64,6 +64,7 @@ protected void configureSitebricks() { install(new StatModule("/stats")); converter(new DateConverters.DateStringConverter(DEFAULT_DATE_TIME_FORMAT)); + converter(new CustomToStringConverter()); install(new AwareModule() { @Override @@ -113,6 +114,7 @@ private void bindExplicitly() { // templating by extension at("/template").show(DecoratedPage.class); at("/velocitySample").show(VelocitySample.class); + at("/customConvertion").show(CustomConverter.class); embed(HelloWorld.class).as("Hello"); } diff --git a/sitebricks-acceptance-tests/src/main/resources/CustomConverter.html b/sitebricks-acceptance-tests/src/main/resources/CustomConverter.html new file mode 100644 index 00000000..2a96231c --- /dev/null +++ b/sitebricks-acceptance-tests/src/main/resources/CustomConverter.html @@ -0,0 +1,16 @@ + + +custom conversion sample + + +

velocity sample

+ +
+ + + + + + + + \ No newline at end of file diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/CustomConverterAcceptanceTest.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/CustomConverterAcceptanceTest.java new file mode 100644 index 00000000..a12d1896 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/CustomConverterAcceptanceTest.java @@ -0,0 +1,30 @@ +package com.google.sitebricks.acceptance; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.testng.annotations.Test; + +import com.google.sitebricks.acceptance.page.CustomConversionPage; +import com.google.sitebricks.acceptance.util.AcceptanceTest; +import com.google.sitebricks.example.CustomConverter; + +@Test(suiteName = AcceptanceTest.SUITE) +public class CustomConverterAcceptanceTest { + + public void hasConvertedTypes() { + WebDriver driver = AcceptanceTest.createWebDriver(); + CustomConversionPage page = CustomConversionPage.open(driver); + + WebElement testValueInputField = page.getTestValue(); + String testValue = testValueInputField.getAttribute("value"); + assert testValue.equals(CustomConverter.INITIAL_VALUE) : "expected " + CustomConverter.INITIAL_VALUE + " but was " + testValue; + + + String expected = "new value from test"; + testValueInputField.sendKeys(expected); + testValueInputField.submit(); + System.out.println(driver.getPageSource()); + + assert testValueInputField.getAttribute("value").equals(CustomConverter.INITIAL_VALUE + expected) : "expected " + CustomConverter.INITIAL_VALUE + expected + " but was " + testValue; + } +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/CustomConversionPage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/CustomConversionPage.java new file mode 100644 index 00000000..a7aaccf3 --- /dev/null +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/CustomConversionPage.java @@ -0,0 +1,22 @@ +package com.google.sitebricks.acceptance.page; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.PageFactory; + +import com.google.sitebricks.acceptance.util.AcceptanceTest; + +public class CustomConversionPage { + + private WebElement testValue; + + public static CustomConversionPage open(WebDriver driver) { + driver.get(AcceptanceTest.BASE_URL + "/customConvertion"); + return PageFactory.initElements(driver, CustomConversionPage.class); + } + + public WebElement getTestValue() { + return testValue; + } + +} diff --git a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/VelocitySamplePage.java b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/VelocitySamplePage.java index e5d394f2..ae49ecde 100644 --- a/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/VelocitySamplePage.java +++ b/sitebricks-acceptance-tests/src/test/java/com/google/sitebricks/acceptance/page/VelocitySamplePage.java @@ -23,7 +23,7 @@ public String getTitle() { } public boolean hasMessage() { - System.out.println(driver.getPageSource()); +// System.out.println(driver.getPageSource()); return driver.getPageSource().contains(VelocitySample.MSG); } From d84ac686d8d2d26d2db9698bacae66e7bbde3a2d Mon Sep 17 00:00:00 2001 From: james Date: Thu, 17 May 2012 14:33:18 -0500 Subject: [PATCH 20/20] this change to WidgetRoutingDispatcher so that the Page put in Flashcache has the page Instance --- .../com/google/sitebricks/routing/WidgetRoutingDispatcher.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java index 3c86ed15..9ebc2728 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java +++ b/sitebricks/src/main/java/com/google/sitebricks/routing/WidgetRoutingDispatcher.java @@ -114,7 +114,6 @@ else if (redirect instanceof Class) { String redirectUri = targetPage.getUri(); if (redirect == instance) { redirectUri = uri; - targetPage = page; } // should never be null coz it will be validated at compile time.