From f3bfff5febcfb275c9e5dbb1149342f5ecd6e8ba Mon Sep 17 00:00:00 2001 From: Les Vogel Date: Mon, 15 May 2017 22:23:06 -0700 Subject: [PATCH] J8 samples (#665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * j8-samples Some bugs * minor tweaks... 1. Fix guestbook-datastore to release API’s 2. Fix the rest to have 1.3.1 & runtime: java8 * add j8 suffix to artifactId * Fix import order * CheckStyle magic. --- .mvn/wrapper/maven-wrapper.properties | 2 +- appengine-java8/README.md | 101 + appengine-java8/analytics/pom.xml | 119 + .../appengine/analytics/AnalyticsServlet.java | 64 + .../src/main/webapp/WEB-INF/appengine-web.xml | 23 + .../analytics/src/main/webapp/WEB-INF/web.xml | 29 + appengine-java8/appidentity/README.md | 18 + appengine-java8/appidentity/pom.xml | 117 + .../appidentity/IdentityServlet.java | 40 + .../appidentity/SignForAppServlet.java | 112 + .../appengine/appidentity/UrlShortener.java | 80 + .../appidentity/UrlShortenerServlet.java | 73 + .../src/main/webapp/WEB-INF/appengine-web.xml | 5 + .../src/main/webapp/WEB-INF/web.xml | 30 + .../appidentity/IdentityServletTest.java | 79 + .../appidentity/SignForAppServletTest.java | 73 + appengine-java8/cloudsql/README.md | 35 + appengine-java8/cloudsql/pom.xml | 116 + .../appengine/cloudsql/CloudSqlServlet.java | 106 + .../src/main/webapp/WEB-INF/appengine-web.xml | 25 + .../cloudsql/src/main/webapp/WEB-INF/web.xml | 28 + appengine-java8/datastore/README.md | 28 + .../datastore/indexes-exploding/pom.xml | 98 + .../com/example/appengine/IndexesServlet.java | 62 + .../src/main/webapp/WEB-INF/appengine-web.xml | 20 + .../main/webapp/WEB-INF/datastore-indexes.xml | 24 + .../src/main/webapp/WEB-INF/web.xml | 39 + .../example/appengine/IndexesServletTest.java | 104 + .../datastore/indexes-perfect/pom.xml | 98 + .../com/example/appengine/IndexesServlet.java | 98 + .../src/main/webapp/WEB-INF/appengine-web.xml | 20 + .../main/webapp/WEB-INF/datastore-indexes.xml | 27 + .../src/main/webapp/WEB-INF/web.xml | 39 + .../example/appengine/IndexesServletTest.java | 83 + appengine-java8/datastore/indexes/pom.xml | 99 + .../com/example/appengine/IndexesServlet.java | 64 + .../src/main/webapp/WEB-INF/appengine-web.xml | 20 + .../main/webapp/WEB-INF/datastore-indexes.xml | 24 + .../indexes/src/main/webapp/WEB-INF/web.xml | 39 + .../example/appengine/IndexesServletTest.java | 77 + appengine-java8/datastore/pom.xml | 125 + .../example/appengine/AbstractGuestbook.java | 83 + .../appengine/AbstractGuestbookServlet.java | 59 + .../java/com/example/appengine/Greeting.java | 43 + .../java/com/example/appengine/Guestbook.java | 66 + .../example/appengine/GuestbookServlet.java | 25 + .../example/appengine/GuestbookStrong.java | 74 + .../appengine/GuestbookStrongServlet.java | 27 + .../example/appengine/ListPeopleServlet.java | 91 + .../example/appengine/ProjectionServlet.java | 79 + .../com/example/appengine/StartupServlet.java | 120 + .../com/example/appengine/StatsServlet.java | 49 + .../src/main/java/com/example/time/Clock.java | 35 + .../java/com/example/time/SystemClock.java | 38 + .../com/example/time/testing/FakeClock.java | 186 + .../src/main/webapp/WEB-INF/appengine-web.xml | 19 + .../main/webapp/WEB-INF/datastore-indexes.xml | 22 + .../datastore/src/main/webapp/WEB-INF/web.xml | 97 + .../datastore/src/main/webapp/guestbook.jsp | 45 + .../com/example/appengine/EntitiesTest.java | 364 + .../appengine/GuestbookStrongTest.java | 103 + .../com/example/appengine/GuestbookTest.java | 125 + .../com/example/appengine/IndexesTest.java | 94 + .../appengine/ListPeopleServletTest.java | 163 + .../appengine/MetadataEntityGroupTest.java | 163 + .../example/appengine/MetadataKindsTest.java | 121 + .../appengine/MetadataNamespacesTest.java | 153 + .../appengine/MetadataPropertiesTest.java | 234 + .../appengine/ProjectionServletTest.java | 104 + .../com/example/appengine/ProjectionTest.java | 94 + .../com/example/appengine/QueriesTest.java | 856 +++ .../com/example/appengine/ReadPolicyTest.java | 113 + .../example/appengine/StartupServletTest.java | 106 + .../example/appengine/TransactionsTest.java | 309 + .../endpoints-frameworks-v2/README.md | 19 + .../backend/.gitignore | 2 + .../endpoints-frameworks-v2/backend/README.md | 72 + .../endpoints-frameworks-v2/backend/pom.xml | 133 + .../src/main/java/com/example/echo/Echo.java | 173 + .../src/main/java/com/example/echo/Email.java | 30 + .../main/java/com/example/echo/Message.java | 31 + .../src/main/webapp/WEB-INF/appengine-web.xml | 32 + .../main/webapp/WEB-INF/logging.properties | 25 + .../backend/src/main/webapp/WEB-INF/web.xml | 65 + .../migration-example/README.md | 125 + .../migration-example/build.gradle | 84 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../migration-example/gradlew | 172 + .../migration-example/gradlew.bat | 84 + .../migration-example/jenkins.sh | 77 + .../migration-example/pom.xml | 116 + .../com/example/helloendpoints/Constants.java | 28 + .../com/example/helloendpoints/Greetings.java | 88 + .../example/helloendpoints/HelloGreeting.java | 36 + .../src/main/webapp/WEB-INF/appengine-web.xml | 23 + .../main/webapp/WEB-INF/logging.properties | 13 + .../src/main/webapp/WEB-INF/web.xml | 64 + .../bootstrap/css/bootstrap-responsive.css | 1109 +++ .../main/webapp/bootstrap/css/bootstrap.css | 6167 +++++++++++++++++ .../src/main/webapp/css/style.css | 48 + .../src/main/webapp/index.html | 101 + .../src/main/webapp/js/base.js | 208 + .../firebase-event-proxy/README.md | 49 + .../gae-firebase-event-proxy/pom.xml | 103 + .../FirebaseEventProxy.java | 114 + .../ServletContextListenerImpl.java | 40 + .../src/main/webapp/WEB-INF/appengine-web.xml | 25 + .../main/webapp/WEB-INF/logging.properties | 27 + .../src/main/webapp/WEB-INF/web.xml | 25 + .../src/main/webapp/index.jsp | 25 + .../gae-firebase-listener-python/.gitignore | 1 + .../gae-firebase-listener-python/app.yaml | 7 + .../gae-firebase-listener-python/main.py | 38 + appengine-java8/firebase-tictactoe/README.md | 53 + appengine-java8/firebase-tictactoe/pom.xml | 127 + .../appengine/firetactoe/DeleteServlet.java | 47 + .../appengine/firetactoe/FirebaseChannel.java | 235 + .../example/appengine/firetactoe/Game.java | 183 + .../appengine/firetactoe/MoveServlet.java | 52 + .../appengine/firetactoe/ObjectifyHelper.java | 37 + .../appengine/firetactoe/OpenedServlet.java | 40 + .../firetactoe/TicTacToeServlet.java | 106 + .../src/main/webapp/WEB-INF/appengine-web.xml | 34 + .../main/webapp/WEB-INF/logging.properties | 27 + .../webapp/WEB-INF/view/firebase_config.jspf | 3 + .../src/main/webapp/WEB-INF/view/index.jsp | 59 + .../src/main/webapp/WEB-INF/web.xml | 77 + .../src/main/webapp/static/main.css | 82 + .../src/main/webapp/static/main.js | 178 + .../firetactoe/DeleteServletTest.java | 155 + .../firetactoe/FirebaseChannelTest.java | 254 + .../appengine/firetactoe/MoveServletTest.java | 164 + .../firetactoe/OpenedServletTest.java | 144 + .../firetactoe/TicTacToeServletTest.java | 209 + .../guestbook-cloud-datastore/README.md | 26 + .../guestbook-cloud-datastore/pom.xml | 113 + .../java/com/example/guestbook/Greeting.java | 124 + .../java/com/example/guestbook/Guestbook.java | 98 + .../com/example/guestbook/Persistence.java | 43 + .../guestbook/SignGuestbookServlet.java | 50 + .../src/main/webapp/WEB-INF/appengine-web.xml | 9 + .../src/main/webapp/WEB-INF/index.yaml | 7 + .../main/webapp/WEB-INF/logging.properties | 12 + .../src/main/webapp/WEB-INF/web.xml | 22 + .../src/main/webapp/guestbook.jsp | 98 + .../src/main/webapp/stylesheets/main.css | 4 + .../com/example/guestbook/GreetingTest.java | 50 + .../guestbook/SignGuestbookServletTest.java | 70 + .../java/com/example/guestbook/TestUtils.java | 50 + appengine-java8/guestbook-objectify/README.md | 17 + appengine-java8/guestbook-objectify/pom.xml | 126 + .../java/com/example/guestbook/Greeting.java | 80 + .../java/com/example/guestbook/Guestbook.java | 33 + .../java/com/example/guestbook/OfyHelper.java | 37 + .../guestbook/SignGuestbookServlet.java | 62 + .../src/main/webapp/WEB-INF/appengine-web.xml | 9 + .../main/webapp/WEB-INF/logging.properties | 13 + .../src/main/webapp/WEB-INF/web.xml | 35 + .../src/main/webapp/guestbook.jsp | 106 + .../src/main/webapp/stylesheets/main.css | 4 + .../com/example/guestbook/GreetingTest.java | 84 + .../guestbook/GuestbookTestUtilities.java | 43 + .../guestbook/SignGuestbookServletTest.java | 118 + appengine-java8/helloworld/README.md | 18 + appengine-java8/helloworld/jenkins.sh | 21 + appengine-java8/helloworld/pom.xml | 54 + .../appengine/helloworld/HelloServlet.java | 36 + .../src/main/webapp/WEB-INF/appengine-web.xml | 19 + .../src/main/webapp/WEB-INF/web.xml | 14 + appengine-java8/images/README.md | 36 + appengine-java8/images/pom.xml | 70 + .../appengine/images/ImagesServlet.java | 118 + .../src/main/webapp/WEB-INF/appengine-web.xml | 19 + .../images/src/main/webapp/WEB-INF/image.jpg | Bin 0 -> 7933 bytes .../images/src/main/webapp/WEB-INF/web.xml | 14 + appengine-java8/logs/README.md | 18 + appengine-java8/logs/pom.xml | 76 + .../example/appengine/logs/LogsServlet.java | 97 + .../src/main/webapp/WEB-INF/appengine-web.xml | 22 + .../main/webapp/WEB-INF/logging.properties | 14 + .../logs/src/main/webapp/WEB-INF/web.xml | 28 + appengine-java8/mail/README.md | 21 + appengine-java8/mail/pom.xml | 65 + .../appengine/mail/BounceHandlerServlet.java | 54 + .../appengine/mail/HandleDiscussionEmail.java | 43 + .../appengine/mail/MailHandlerBase.java | 116 + .../appengine/mail/MailHandlerServlet.java | 49 + .../example/appengine/mail/MailServlet.java | 125 + .../src/main/webapp/WEB-INF/appengine-web.xml | 28 + .../main/webapp/WEB-INF/logging.properties | 27 + .../mail/src/main/webapp/WEB-INF/web.xml | 79 + appengine-java8/mailgun/README.md | 11 + appengine-java8/mailgun/pom.xml | 69 + .../appengine/mailgun/MailgunServlet.java | 92 + .../src/main/webapp/WEB-INF/appengine-web.xml | 24 + .../mailgun/src/main/webapp/WEB-INF/web.xml | 29 + .../mailgun/src/main/webapp/index.html | 27 + appengine-java8/mailjet/README.md | 15 + appengine-java8/mailjet/pom.xml | 79 + .../appengine/mailjet/MailjetServlet.java | 73 + .../src/main/webapp/WEB-INF/appengine-web.xml | 25 + .../mailjet/src/main/webapp/WEB-INF/web.xml | 28 + .../mailjet/src/main/webapp/index.html | 27 + appengine-java8/memcache/pom.xml | 68 + .../memcache/MemcacheAsyncCacheServlet.java | 75 + .../memcache/MemcacheBestPracticeServlet.java | 57 + .../memcache/MemcacheConcurrentServlet.java | 85 + .../memcache/MemcacheSyncCacheServlet.java | 67 + .../src/main/webapp/WEB-INF/appengine-web.xml | 20 + .../memcache/src/main/webapp/WEB-INF/web.xml | 53 + appengine-java8/multitenancy/README.md | 19 + appengine-java8/multitenancy/pom.xml | 127 + .../java/com/example/appengine/Greeting.java | 76 + .../java/com/example/appengine/Guestbook.java | 33 + .../appengine/MultitenancyServlet.java | 114 + .../example/appengine/NamespaceFilter.java | 52 + .../java/com/example/appengine/OfyHelper.java | 40 + .../appengine/SignGuestbookServlet.java | 61 + .../example/appengine/SomeRequestServlet.java | 55 + .../appengine/UpdateCountsServlet.java | 99 + .../src/main/webapp/WEB-INF/appengine-web.xml | 9 + .../main/webapp/WEB-INF/logging.properties | 13 + .../src/main/webapp/WEB-INF/web.xml | 49 + .../src/main/webapp/guestbook.jsp | 106 + .../src/main/webapp/stylesheets/main.css | 4 + .../com/example/appengine/GreetingTest.java | 84 + .../appengine/GuestbookTestUtilities.java | 43 + .../appengine/SignGuestbookServletTest.java | 118 + appengine-java8/oauth2/README.md | 41 + appengine-java8/oauth2/pom.xml | 67 + .../com/example/appengine/HelloServlet.java | 56 + .../com/example/appengine/Oauth2Filter.java | 96 + .../src/main/webapp/WEB-INF/appengine-web.xml | 19 + .../oauth2/src/main/webapp/WEB-INF/web.xml | 27 + .../oauth2/src/main/webapp/index.html | 53 + appengine-java8/pom.xml | 87 + appengine-java8/remote/README.md | 21 + appengine-java8/remote/remote-client/pom.xml | 72 + .../appengine/remote/RemoteApiExample.java | 50 + appengine-java8/remote/remote-server/pom.xml | 68 + .../appengine/remote/RemoteServlet.java | 36 + .../src/main/webapp/WEB-INF/appengine-web.xml | 20 + .../src/main/webapp/WEB-INF/web.xml | 41 + appengine-java8/requests/README.md | 31 + appengine-java8/requests/pom.xml | 112 + .../appengine/requests/LoggingServlet.java | 41 + .../appengine/requests/RequestsServlet.java | 33 + .../src/main/webapp/WEB-INF/appengine-web.xml | 5 + .../requests/src/main/webapp/WEB-INF/web.xml | 26 + .../requests/LoggingServletTest.java | 85 + .../requests/RequestsServletTest.java | 65 + appengine-java8/search/README.md | 20 + appengine-java8/search/pom.xml | 96 + .../appengine/search/DeleteServlet.java | 94 + .../appengine/search/DocumentServlet.java | 83 + .../appengine/search/IndexServlet.java | 75 + .../appengine/search/SchemaServlet.java | 81 + .../appengine/search/SearchOptionServlet.java | 126 + .../appengine/search/SearchServlet.java | 117 + .../com/example/appengine/search/Utils.java | 63 + .../src/main/webapp/WEB-INF/appengine-web.xml | 19 + .../search/src/main/webapp/WEB-INF/web.xml | 54 + .../appengine/search/DeleteServletTest.java | 66 + .../appengine/search/DocumentServletTest.java | 105 + .../appengine/search/IndexServletTest.java | 66 + .../appengine/search/SchemaServletTest.java | 76 + .../search/SearchOptionServletTest.java | 66 + .../appengine/search/SearchServletTest.java | 70 + .../example/appengine/search/UtilsTest.java | 62 + appengine-java8/sendgrid/README.md | 32 + appengine-java8/sendgrid/pom.xml | 62 + .../appengine/sendgrid/SendEmailServlet.java | 64 + .../src/main/webapp/WEB-INF/appengine-web.xml | 24 + .../sendgrid/src/main/webapp/WEB-INF/web.xml | 29 + appengine-java8/static-files/pom.xml | 54 + .../src/main/webapp/WEB-INF/appengine-web.xml | 20 + .../src/main/webapp/WEB-INF/web.xml | 24 + .../static-files/src/main/webapp/index.html | 10 + .../src/main/webapp/stylesheets/styles.css | 4 + appengine-java8/taskqueue/README.md | 5 + appengine-java8/taskqueue/deferred/README.md | 24 + appengine-java8/taskqueue/deferred/pom.xml | 98 + .../taskqueue/samples/DeferSampleServlet.java | 76 + .../src/main/webapp/WEB-INF/appengine-web.xml | 25 + .../main/webapp/WEB-INF/logging.properties | 13 + .../deferred/src/main/webapp/WEB-INF/web.xml | 30 + .../deferred/src/main/webapp/guestbook.jsp | 110 + appengine-java8/taskqueue/pull/README.md | 23 + appengine-java8/taskqueue/pull/pom.xml | 80 + .../example/taskqueue/TaskqueueServlet.java | 135 + .../src/main/webapp/WEB-INF/appengine-web.xml | 19 + .../main/webapp/WEB-INF/logging.properties | 27 + .../pull/src/main/webapp/WEB-INF/queue.xml | 11 + .../pull/src/main/webapp/WEB-INF/web.xml | 33 + .../taskqueue/pull/src/main/webapp/tasks.jsp | 50 + appengine-java8/taskqueue/push/README.md | 25 + appengine-java8/taskqueue/push/pom.xml | 103 + .../appengine/taskqueue/push/Enqueue.java | 42 + .../appengine/taskqueue/push/Worker.java | 40 + .../src/main/webapp/WEB-INF/appengine-web.xml | 20 + .../push/src/main/webapp/WEB-INF/web.xml | 39 + .../taskqueue/push/src/main/webapp/index.html | 28 + .../appengine/taskqueue/push/WorkerTest.java | 79 + appengine-java8/twilio/README.md | 37 + appengine-java8/twilio/pom.xml | 63 + .../appengine/twilio/ReceiveCallServlet.java | 49 + .../appengine/twilio/ReceiveSmsServlet.java | 53 + .../appengine/twilio/SendSmsServlet.java | 67 + .../src/main/webapp/WEB-INF/appengine-web.xml | 24 + .../twilio/src/main/webapp/WEB-INF/web.xml | 45 + appengine-java8/unittests/README.md | 9 + appengine-java8/unittests/pom.xml | 94 + .../src/main/webapp/WEB-INF/appengine-web.xml | 9 + .../main/webapp/WEB-INF/logging.properties | 13 + .../src/main/webapp/WEB-INF/queue.xml | 10 + .../unittests/src/main/webapp/WEB-INF/web.xml | 7 + .../appengine/samples/AuthenticationTest.java | 54 + .../appengine/samples/DeferredTaskTest.java | 76 + ...LocalCustomPolicyHighRepDatastoreTest.java | 81 + .../appengine/samples/LocalDatastoreTest.java | 70 + .../samples/LocalHighRepDatastoreTest.java | 66 + .../appengine/samples/LocalMemcacheTest.java | 72 + .../appengine/samples/LocalUrlFetchTest.java | 77 + .../google/appengine/samples/MyFirstTest.java | 31 + .../google/appengine/samples/ShortTest.java | 59 + .../samples/TaskQueueConfigTest.java | 75 + .../appengine/samples/TaskQueueTest.java | 73 + appengine-java8/urlfetch/.gitignore | 11 + appengine-java8/urlfetch/README.md | 18 + appengine-java8/urlfetch/pom.xml | 61 + .../example/appengine/UrlFetchServlet.java | 103 + .../src/main/webapp/WEB-INF/appengine-web.xml | 5 + .../urlfetch/src/main/webapp/WEB-INF/web.xml | 14 + .../urlfetch/src/main/webapp/main.jsp | 48 + appengine-java8/users/README.md | 20 + appengine-java8/users/pom.xml | 113 + .../example/appengine/users/UsersServlet.java | 51 + .../src/main/webapp/WEB-INF/appengine-web.xml | 5 + .../users/src/main/webapp/WEB-INF/web.xml | 14 + .../appengine/users/UsersServletTest.java | 109 + appengine/.gitignore | 44 - appengine/appidentity/README.md | 21 +- appengine/appidentity/pom.xml | 12 +- .../src/main/webapp/WEB-INF/appengine-web.xml | 1 - appengine/channel/README.md | 5 + pom.xml | 1 + 346 files changed, 29175 insertions(+), 68 deletions(-) create mode 100644 appengine-java8/README.md create mode 100644 appengine-java8/analytics/pom.xml create mode 100644 appengine-java8/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java create mode 100644 appengine-java8/analytics/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/analytics/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/appidentity/README.md create mode 100644 appengine-java8/appidentity/pom.xml create mode 100644 appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/IdentityServlet.java create mode 100644 appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/SignForAppServlet.java create mode 100644 appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortener.java create mode 100644 appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortenerServlet.java create mode 100644 appengine-java8/appidentity/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/appidentity/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java create mode 100644 appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java create mode 100644 appengine-java8/cloudsql/README.md create mode 100644 appengine-java8/cloudsql/pom.xml create mode 100644 appengine-java8/cloudsql/src/main/java/com/example/appengine/cloudsql/CloudSqlServlet.java create mode 100644 appengine-java8/cloudsql/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/cloudsql/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/datastore/README.md create mode 100644 appengine-java8/datastore/indexes-exploding/pom.xml create mode 100644 appengine-java8/datastore/indexes-exploding/src/main/java/com/example/appengine/IndexesServlet.java create mode 100644 appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/datastore-indexes.xml create mode 100644 appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/datastore/indexes-exploding/src/test/java/com/example/appengine/IndexesServletTest.java create mode 100644 appengine-java8/datastore/indexes-perfect/pom.xml create mode 100644 appengine-java8/datastore/indexes-perfect/src/main/java/com/example/appengine/IndexesServlet.java create mode 100644 appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/datastore-indexes.xml create mode 100644 appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/datastore/indexes-perfect/src/test/java/com/example/appengine/IndexesServletTest.java create mode 100644 appengine-java8/datastore/indexes/pom.xml create mode 100644 appengine-java8/datastore/indexes/src/main/java/com/example/appengine/IndexesServlet.java create mode 100644 appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/datastore-indexes.xml create mode 100644 appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/datastore/indexes/src/test/java/com/example/appengine/IndexesServletTest.java create mode 100644 appengine-java8/datastore/pom.xml create mode 100644 appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/appengine/Greeting.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/appengine/Guestbook.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookServlet.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrong.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/appengine/ProjectionServlet.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/appengine/StartupServlet.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/appengine/StatsServlet.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/time/Clock.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/time/SystemClock.java create mode 100644 appengine-java8/datastore/src/main/java/com/example/time/testing/FakeClock.java create mode 100644 appengine-java8/datastore/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml create mode 100644 appengine-java8/datastore/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/datastore/src/main/webapp/guestbook.jsp create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/EntitiesTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/GuestbookStrongTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/GuestbookTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/IndexesTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/ListPeopleServletTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/MetadataEntityGroupTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/MetadataKindsTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/MetadataNamespacesTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/MetadataPropertiesTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/ProjectionServletTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/ProjectionTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/QueriesTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/ReadPolicyTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/StartupServletTest.java create mode 100644 appengine-java8/datastore/src/test/java/com/example/appengine/TransactionsTest.java create mode 100644 appengine-java8/endpoints-frameworks-v2/README.md create mode 100644 appengine-java8/endpoints-frameworks-v2/backend/.gitignore create mode 100644 appengine-java8/endpoints-frameworks-v2/backend/README.md create mode 100644 appengine-java8/endpoints-frameworks-v2/backend/pom.xml create mode 100644 appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Echo.java create mode 100644 appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Email.java create mode 100644 appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Message.java create mode 100644 appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/README.md create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/build.gradle create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/gradle/wrapper/gradle-wrapper.properties create mode 100755 appengine-java8/endpoints-frameworks-v2/migration-example/gradlew create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/gradlew.bat create mode 100755 appengine-java8/endpoints-frameworks-v2/migration-example/jenkins.sh create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/pom.xml create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/Constants.java create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/Greetings.java create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/HelloGreeting.java create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/bootstrap/css/bootstrap-responsive.css create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/bootstrap/css/bootstrap.css create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/css/style.css create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/index.html create mode 100644 appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/js/base.js create mode 100644 appengine-java8/firebase-event-proxy/README.md create mode 100644 appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/pom.xml create mode 100644 appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/FirebaseEventProxy.java create mode 100644 appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/ServletContextListenerImpl.java create mode 100644 appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/index.jsp create mode 100644 appengine-java8/firebase-event-proxy/gae-firebase-listener-python/.gitignore create mode 100644 appengine-java8/firebase-event-proxy/gae-firebase-listener-python/app.yaml create mode 100644 appengine-java8/firebase-event-proxy/gae-firebase-listener-python/main.py create mode 100644 appengine-java8/firebase-tictactoe/README.md create mode 100644 appengine-java8/firebase-tictactoe/pom.xml create mode 100644 appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java create mode 100644 appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java create mode 100644 appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/Game.java create mode 100644 appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java create mode 100644 appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/ObjectifyHelper.java create mode 100644 appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java create mode 100644 appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java create mode 100644 appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/firebase_config.jspf create mode 100644 appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/index.jsp create mode 100644 appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/firebase-tictactoe/src/main/webapp/static/main.css create mode 100644 appengine-java8/firebase-tictactoe/src/main/webapp/static/main.js create mode 100644 appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/DeleteServletTest.java create mode 100644 appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/FirebaseChannelTest.java create mode 100644 appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/MoveServletTest.java create mode 100644 appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/OpenedServletTest.java create mode 100644 appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java create mode 100644 appengine-java8/guestbook-cloud-datastore/README.md create mode 100644 appengine-java8/guestbook-cloud-datastore/pom.xml create mode 100644 appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Greeting.java create mode 100644 appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Guestbook.java create mode 100644 appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Persistence.java create mode 100644 appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/SignGuestbookServlet.java create mode 100644 appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/index.yaml create mode 100644 appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/guestbook-cloud-datastore/src/main/webapp/guestbook.jsp create mode 100644 appengine-java8/guestbook-cloud-datastore/src/main/webapp/stylesheets/main.css create mode 100644 appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/GreetingTest.java create mode 100644 appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/SignGuestbookServletTest.java create mode 100644 appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/TestUtils.java create mode 100644 appengine-java8/guestbook-objectify/README.md create mode 100644 appengine-java8/guestbook-objectify/pom.xml create mode 100644 appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Greeting.java create mode 100644 appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Guestbook.java create mode 100644 appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/OfyHelper.java create mode 100644 appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/SignGuestbookServlet.java create mode 100644 appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/guestbook-objectify/src/main/webapp/guestbook.jsp create mode 100644 appengine-java8/guestbook-objectify/src/main/webapp/stylesheets/main.css create mode 100644 appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GreetingTest.java create mode 100644 appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GuestbookTestUtilities.java create mode 100644 appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/SignGuestbookServletTest.java create mode 100644 appengine-java8/helloworld/README.md create mode 100644 appengine-java8/helloworld/jenkins.sh create mode 100644 appengine-java8/helloworld/pom.xml create mode 100644 appengine-java8/helloworld/src/main/java/com/example/appengine/helloworld/HelloServlet.java create mode 100644 appengine-java8/helloworld/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/helloworld/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/images/README.md create mode 100644 appengine-java8/images/pom.xml create mode 100644 appengine-java8/images/src/main/java/com/example/appengine/images/ImagesServlet.java create mode 100644 appengine-java8/images/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/images/src/main/webapp/WEB-INF/image.jpg create mode 100644 appengine-java8/images/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/logs/README.md create mode 100644 appengine-java8/logs/pom.xml create mode 100644 appengine-java8/logs/src/main/java/com/example/appengine/logs/LogsServlet.java create mode 100644 appengine-java8/logs/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/logs/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/logs/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/mail/README.md create mode 100644 appengine-java8/mail/pom.xml create mode 100644 appengine-java8/mail/src/main/java/com/example/appengine/mail/BounceHandlerServlet.java create mode 100644 appengine-java8/mail/src/main/java/com/example/appengine/mail/HandleDiscussionEmail.java create mode 100644 appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerBase.java create mode 100644 appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerServlet.java create mode 100644 appengine-java8/mail/src/main/java/com/example/appengine/mail/MailServlet.java create mode 100644 appengine-java8/mail/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/mail/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/mail/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/mailgun/README.md create mode 100644 appengine-java8/mailgun/pom.xml create mode 100644 appengine-java8/mailgun/src/main/java/com/example/appengine/mailgun/MailgunServlet.java create mode 100644 appengine-java8/mailgun/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/mailgun/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/mailgun/src/main/webapp/index.html create mode 100644 appengine-java8/mailjet/README.md create mode 100644 appengine-java8/mailjet/pom.xml create mode 100644 appengine-java8/mailjet/src/main/java/com/example/appengine/mailjet/MailjetServlet.java create mode 100644 appengine-java8/mailjet/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/mailjet/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/mailjet/src/main/webapp/index.html create mode 100644 appengine-java8/memcache/pom.xml create mode 100644 appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheAsyncCacheServlet.java create mode 100644 appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheBestPracticeServlet.java create mode 100644 appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheConcurrentServlet.java create mode 100644 appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheSyncCacheServlet.java create mode 100644 appengine-java8/memcache/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/memcache/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/multitenancy/README.md create mode 100644 appengine-java8/multitenancy/pom.xml create mode 100644 appengine-java8/multitenancy/src/main/java/com/example/appengine/Greeting.java create mode 100644 appengine-java8/multitenancy/src/main/java/com/example/appengine/Guestbook.java create mode 100644 appengine-java8/multitenancy/src/main/java/com/example/appengine/MultitenancyServlet.java create mode 100644 appengine-java8/multitenancy/src/main/java/com/example/appengine/NamespaceFilter.java create mode 100644 appengine-java8/multitenancy/src/main/java/com/example/appengine/OfyHelper.java create mode 100644 appengine-java8/multitenancy/src/main/java/com/example/appengine/SignGuestbookServlet.java create mode 100644 appengine-java8/multitenancy/src/main/java/com/example/appengine/SomeRequestServlet.java create mode 100644 appengine-java8/multitenancy/src/main/java/com/example/appengine/UpdateCountsServlet.java create mode 100644 appengine-java8/multitenancy/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/multitenancy/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/multitenancy/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/multitenancy/src/main/webapp/guestbook.jsp create mode 100644 appengine-java8/multitenancy/src/main/webapp/stylesheets/main.css create mode 100644 appengine-java8/multitenancy/src/test/java/com/example/appengine/GreetingTest.java create mode 100644 appengine-java8/multitenancy/src/test/java/com/example/appengine/GuestbookTestUtilities.java create mode 100644 appengine-java8/multitenancy/src/test/java/com/example/appengine/SignGuestbookServletTest.java create mode 100644 appengine-java8/oauth2/README.md create mode 100644 appengine-java8/oauth2/pom.xml create mode 100644 appengine-java8/oauth2/src/main/java/com/example/appengine/HelloServlet.java create mode 100644 appengine-java8/oauth2/src/main/java/com/example/appengine/Oauth2Filter.java create mode 100644 appengine-java8/oauth2/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/oauth2/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/oauth2/src/main/webapp/index.html create mode 100644 appengine-java8/pom.xml create mode 100644 appengine-java8/remote/README.md create mode 100644 appengine-java8/remote/remote-client/pom.xml create mode 100644 appengine-java8/remote/remote-client/src/main/java/com/example/appengine/remote/RemoteApiExample.java create mode 100644 appengine-java8/remote/remote-server/pom.xml create mode 100644 appengine-java8/remote/remote-server/src/main/java/com/example/appengine/remote/RemoteServlet.java create mode 100644 appengine-java8/remote/remote-server/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/remote/remote-server/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/requests/README.md create mode 100644 appengine-java8/requests/pom.xml create mode 100644 appengine-java8/requests/src/main/java/com/example/appengine/requests/LoggingServlet.java create mode 100644 appengine-java8/requests/src/main/java/com/example/appengine/requests/RequestsServlet.java create mode 100644 appengine-java8/requests/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/requests/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/requests/src/test/java/com/example/appengine/requests/LoggingServletTest.java create mode 100644 appengine-java8/requests/src/test/java/com/example/appengine/requests/RequestsServletTest.java create mode 100644 appengine-java8/search/README.md create mode 100644 appengine-java8/search/pom.xml create mode 100644 appengine-java8/search/src/main/java/com/example/appengine/search/DeleteServlet.java create mode 100644 appengine-java8/search/src/main/java/com/example/appengine/search/DocumentServlet.java create mode 100644 appengine-java8/search/src/main/java/com/example/appengine/search/IndexServlet.java create mode 100644 appengine-java8/search/src/main/java/com/example/appengine/search/SchemaServlet.java create mode 100644 appengine-java8/search/src/main/java/com/example/appengine/search/SearchOptionServlet.java create mode 100644 appengine-java8/search/src/main/java/com/example/appengine/search/SearchServlet.java create mode 100644 appengine-java8/search/src/main/java/com/example/appengine/search/Utils.java create mode 100644 appengine-java8/search/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/search/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/search/src/test/java/com/example/appengine/search/DeleteServletTest.java create mode 100644 appengine-java8/search/src/test/java/com/example/appengine/search/DocumentServletTest.java create mode 100644 appengine-java8/search/src/test/java/com/example/appengine/search/IndexServletTest.java create mode 100644 appengine-java8/search/src/test/java/com/example/appengine/search/SchemaServletTest.java create mode 100644 appengine-java8/search/src/test/java/com/example/appengine/search/SearchOptionServletTest.java create mode 100644 appengine-java8/search/src/test/java/com/example/appengine/search/SearchServletTest.java create mode 100644 appengine-java8/search/src/test/java/com/example/appengine/search/UtilsTest.java create mode 100644 appengine-java8/sendgrid/README.md create mode 100644 appengine-java8/sendgrid/pom.xml create mode 100644 appengine-java8/sendgrid/src/main/java/com/example/appengine/sendgrid/SendEmailServlet.java create mode 100644 appengine-java8/sendgrid/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/sendgrid/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/static-files/pom.xml create mode 100644 appengine-java8/static-files/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/static-files/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/static-files/src/main/webapp/index.html create mode 100644 appengine-java8/static-files/src/main/webapp/stylesheets/styles.css create mode 100644 appengine-java8/taskqueue/README.md create mode 100644 appengine-java8/taskqueue/deferred/README.md create mode 100644 appengine-java8/taskqueue/deferred/pom.xml create mode 100644 appengine-java8/taskqueue/deferred/src/main/java/com/google/cloud/taskqueue/samples/DeferSampleServlet.java create mode 100644 appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/taskqueue/deferred/src/main/webapp/guestbook.jsp create mode 100644 appengine-java8/taskqueue/pull/README.md create mode 100644 appengine-java8/taskqueue/pull/pom.xml create mode 100644 appengine-java8/taskqueue/pull/src/main/java/com/example/taskqueue/TaskqueueServlet.java create mode 100644 appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/queue.xml create mode 100644 appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/taskqueue/pull/src/main/webapp/tasks.jsp create mode 100644 appengine-java8/taskqueue/push/README.md create mode 100644 appengine-java8/taskqueue/push/pom.xml create mode 100644 appengine-java8/taskqueue/push/src/main/java/com/example/appengine/taskqueue/push/Enqueue.java create mode 100644 appengine-java8/taskqueue/push/src/main/java/com/example/appengine/taskqueue/push/Worker.java create mode 100644 appengine-java8/taskqueue/push/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/taskqueue/push/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/taskqueue/push/src/main/webapp/index.html create mode 100644 appengine-java8/taskqueue/push/src/test/java/com/example/appengine/taskqueue/push/WorkerTest.java create mode 100644 appengine-java8/twilio/README.md create mode 100644 appengine-java8/twilio/pom.xml create mode 100644 appengine-java8/twilio/src/main/java/com/example/appengine/twilio/ReceiveCallServlet.java create mode 100644 appengine-java8/twilio/src/main/java/com/example/appengine/twilio/ReceiveSmsServlet.java create mode 100644 appengine-java8/twilio/src/main/java/com/example/appengine/twilio/SendSmsServlet.java create mode 100644 appengine-java8/twilio/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/twilio/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/unittests/README.md create mode 100644 appengine-java8/unittests/pom.xml create mode 100644 appengine-java8/unittests/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/unittests/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine-java8/unittests/src/main/webapp/WEB-INF/queue.xml create mode 100644 appengine-java8/unittests/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/unittests/src/test/java/com/google/appengine/samples/AuthenticationTest.java create mode 100644 appengine-java8/unittests/src/test/java/com/google/appengine/samples/DeferredTaskTest.java create mode 100644 appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalCustomPolicyHighRepDatastoreTest.java create mode 100644 appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalDatastoreTest.java create mode 100644 appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalHighRepDatastoreTest.java create mode 100644 appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalMemcacheTest.java create mode 100644 appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalUrlFetchTest.java create mode 100644 appengine-java8/unittests/src/test/java/com/google/appengine/samples/MyFirstTest.java create mode 100644 appengine-java8/unittests/src/test/java/com/google/appengine/samples/ShortTest.java create mode 100644 appengine-java8/unittests/src/test/java/com/google/appengine/samples/TaskQueueConfigTest.java create mode 100644 appengine-java8/unittests/src/test/java/com/google/appengine/samples/TaskQueueTest.java create mode 100644 appengine-java8/urlfetch/.gitignore create mode 100644 appengine-java8/urlfetch/README.md create mode 100644 appengine-java8/urlfetch/pom.xml create mode 100644 appengine-java8/urlfetch/src/main/java/com/example/appengine/UrlFetchServlet.java create mode 100644 appengine-java8/urlfetch/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/urlfetch/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/urlfetch/src/main/webapp/main.jsp create mode 100644 appengine-java8/users/README.md create mode 100644 appengine-java8/users/pom.xml create mode 100644 appengine-java8/users/src/main/java/com/example/appengine/users/UsersServlet.java create mode 100644 appengine-java8/users/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine-java8/users/src/main/webapp/WEB-INF/web.xml create mode 100644 appengine-java8/users/src/test/java/com/example/appengine/users/UsersServletTest.java delete mode 100644 appengine/.gitignore create mode 100644 appengine/channel/README.md diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 6637cedb28e..c3150437037 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1 @@ -distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip \ No newline at end of file +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip diff --git a/appengine-java8/README.md b/appengine-java8/README.md new file mode 100644 index 00000000000..62f932bda7e --- /dev/null +++ b/appengine-java8/README.md @@ -0,0 +1,101 @@ +# Google App Engine Standard Environment Samples for Java 8 + +This is a repository that contains Java code samples for [Google App Engine +standard environment][ae-docs]. + +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Prerequisites + +### Download Maven + +These samples use the [Apache Maven][maven] build system. Before getting +started, be sure to [download][maven-download] and [install][maven-install] it. +When you use Maven as described here, it will automatically download the needed +client libraries. + +[maven]: https://maven.apache.org +[maven-download]: https://maven.apache.org/download.cgi +[maven-install]: https://maven.apache.org/install.html + +### Create a Project in the Google Cloud Platform Console + +If you haven't already created a project, create one now. Projects enable you to +manage all Google Cloud Platform resources for your app, including deployment, +access control, billing, and services. + +1. Open the [Cloud Platform Console][cloud-console]. +1. In the drop-down menu at the top, select **Create a project**. +1. Give your project a name. +1. Make a note of the project ID, which might be different from the project + name. The project ID is used in commands and in configurations. + +[cloud-console]: https://console.cloud.google.com/ + + +## Samples + +### Hello World + +This sample demonstrates how to deploy an application on Google App Engine. + +- [Documentation][ae-docs] +- [Code](helloworld) + +### Sending Email + +#### Sending Email with Mailgun + +This sample demonstrates how to send email using the [Mailgun API][mailgun-api]. + +- [Documentation][mailgun-sample-docs] +- [Code](mailgun) + +[mailgun-api]: https://documentation.mailgun.com/ +[mailgun-sample-docs]: https://cloud.google.com/appengine/docs/java/mail/mailgun + +#### Sending Email with SendGrid + +This sample demonstrates how to send email using the [SendGrid][sendgrid]. + +- [Documentation][sendgrid-sample-docs] +- [Code](sendgrid) + +[sendgrid]: https://sendgrid.com/docs/User_Guide/index.html +[sendgrid-sample-docs]: https://cloud.google.com/appengine/docs/java/mail/sendgrid + +### Sending SMS with Twilio + +This sample demonstrates how to use [Twilio](https://www.twilio.com) on [Google +App Engine standard environment][ae-docs]. + +- [Documentation][twilio-sample-docs] +- [Code](twilio) + +[twilio-sample-docs]: https://cloud.google.com/appengine/docs/java/sms/twilio + +### App Identity + +This sample demonstrates how to use the [App Identity API][appid] to discover +the application's ID and assert identity to Google and third-party APIs. + +- [Documentation][appid] +- [Code](appidentity) + +[appid]: https://cloud.google.com/appengine/docs/java/appidentity/ + +### Other Samples + +- [Sample Applications][sample-apps] + +[sample-apps]: https://cloud.google.com/appengine/docs/java/samples + + +## Contributing changes + +See [CONTRIBUTING.md](../CONTRIBUTING.md). + +## Licensing + +See [LICENSE](../LICENSE). + diff --git a/appengine-java8/analytics/pom.xml b/appengine-java8/analytics/pom.xml new file mode 100644 index 00000000000..b4080ef1d4f --- /dev/null +++ b/appengine-java8/analytics/pom.xml @@ -0,0 +1,119 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-analytics-j8 + + + appengine-java8-samples + com.google.cloud + 1.0.0 + .. + + + + + 1.8 + 1.8 + 1.9.52 + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + jstl + jstl + 1.2 + + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.7.22 + test + + + com.jcabi + jcabi-matchers + 1.4 + + + com.google.truth + truth + 0.32 + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java b/appengine-java8/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java new file mode 100644 index 00000000000..4fd2d6186b6 --- /dev/null +++ b/appengine-java8/analytics/src/main/java/com/example/appengine/analytics/AnalyticsServlet.java @@ -0,0 +1,64 @@ +/** + * Copyright 2015 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.example.appengine.analytics; + +import com.google.appengine.api.urlfetch.URLFetchService; +import com.google.appengine.api.urlfetch.URLFetchServiceFactory; + +import org.apache.http.client.utils.URIBuilder; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class AnalyticsServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + String trackingId = System.getenv("GA_TRACKING_ID"); + URIBuilder builder = new URIBuilder(); + builder.setScheme("http").setHost("www.google-analytics.com").setPath("/collect") + .addParameter("v", "1") // API Version. + .addParameter("tid", trackingId) // Tracking ID / Property ID. + // Anonymous Client Identifier. Ideally, this should be a UUID that + // is associated with particular user, device, or browser instance. + .addParameter("cid", "555") + .addParameter("t", "event") // Event hit type. + .addParameter("ec", "example") // Event category. + .addParameter("ea", "test action"); // Event action. + URI uri = null; + try { + uri = builder.build(); + } catch (URISyntaxException e) { + throw new ServletException("Problem building URI", e); + } + URLFetchService fetcher = URLFetchServiceFactory.getURLFetchService(); + URL url = uri.toURL(); + fetcher.fetch(url); + resp.getWriter().println("Event tracked."); + } +} +// [END example] diff --git a/appengine-java8/analytics/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/analytics/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..796dfaa0ddd --- /dev/null +++ b/appengine-java8/analytics/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,23 @@ + + + + + + true + java8 + + + + + diff --git a/appengine-java8/analytics/src/main/webapp/WEB-INF/web.xml b/appengine-java8/analytics/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..cb3033ed531 --- /dev/null +++ b/appengine-java8/analytics/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,29 @@ + + + + + + + analytics + com.example.appengine.analytics.AnalyticsServlet + + + analytics + / + + + diff --git a/appengine-java8/appidentity/README.md b/appengine-java8/appidentity/README.md new file mode 100644 index 00000000000..49c1c62a30e --- /dev/null +++ b/appengine-java8/appidentity/README.md @@ -0,0 +1,18 @@ +# App Identity sample for Google App Engine + +This sample demonstrates how to use the [App Identity API][appid] on [Google App +Engine][ae-docs]. + +[appid]: https://cloud.google.com/appengine/docs/java/appidentity/ +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Running locally +This example uses the +[Maven Cloud SDK based plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven). +To run this sample locally: + + $ mvn appengine:run + +## Deploying + + $ mvn appengine:deploy diff --git a/appengine-java8/appidentity/pom.xml b/appengine-java8/appidentity/pom.xml new file mode 100644 index 00000000000..2add6af8a74 --- /dev/null +++ b/appengine-java8/appidentity/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-appidentity-j8 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + 1.8 + 1.8 + 1.9.52 + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + com.google.guava + guava + 20.0 + + + + org.json + json + 20160810 + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.32 + test + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/IdentityServlet.java b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/IdentityServlet.java new file mode 100644 index 00000000000..d9d72a704c9 --- /dev/null +++ b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/IdentityServlet.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015 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.example.appengine.appidentity; + +import com.google.apphosting.api.ApiProxy; + +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@SuppressWarnings("serial") +public class IdentityServlet extends HttpServlet { + + // [START versioned_hostnames] + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setContentType("text/plain"); + ApiProxy.Environment env = ApiProxy.getCurrentEnvironment(); + resp.getWriter().print("default_version_hostname: "); + resp.getWriter() + .println(env.getAttributes().get("com.google.appengine.runtime.default_version_hostname")); + } + // [END versioned_hostnames] +} diff --git a/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/SignForAppServlet.java b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/SignForAppServlet.java new file mode 100644 index 00000000000..3a9df0301fe --- /dev/null +++ b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/SignForAppServlet.java @@ -0,0 +1,112 @@ +/* + * Copyright 2016 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.example.appengine.appidentity; + +import com.google.appengine.api.appidentity.AppIdentityService; +import com.google.appengine.api.appidentity.AppIdentityServiceFactory; +import com.google.appengine.api.appidentity.PublicCertificate; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.Arrays; +import java.util.Collection; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@SuppressWarnings("serial") +public class SignForAppServlet extends HttpServlet { + private final AppIdentityService appIdentity; + + public SignForAppServlet() { + appIdentity = AppIdentityServiceFactory.getAppIdentityService(); + } + + // [START asserting_identity_to_other_services] + // Note that the algorithm used by AppIdentity.signForApp() and + // getPublicCertificatesForApp() is "SHA256withRSA" + + private byte[] signBlob(byte[] blob) { + AppIdentityService.SigningResult result = appIdentity.signForApp(blob); + return result.getSignature(); + } + + private byte[] getPublicCertificate() throws UnsupportedEncodingException { + Collection certs = appIdentity.getPublicCertificatesForApp(); + PublicCertificate publicCert = certs.iterator().next(); + return publicCert.getX509CertificateInPemFormat().getBytes("UTF-8"); + } + + private Certificate parsePublicCertificate(byte[] publicCert) + throws CertificateException, NoSuchAlgorithmException { + InputStream stream = new ByteArrayInputStream(publicCert); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return cf.generateCertificate(stream); + } + + private boolean verifySignature(byte[] blob, byte[] blobSignature, PublicKey pk) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initVerify(pk); + signature.update(blob); + return signature.verify(blobSignature); + } + + private String simulateIdentityAssertion() + throws CertificateException, UnsupportedEncodingException, NoSuchAlgorithmException, + InvalidKeyException, SignatureException { + // Simulate the sending app. + String message = "abcdefg"; + byte[] blob = message.getBytes(); + byte[] blobSignature = signBlob(blob); + byte[] publicCert = getPublicCertificate(); + + // Simulate the receiving app, which gets the certificate, blob, and signature. + Certificate cert = parsePublicCertificate(publicCert); + PublicKey pk = cert.getPublicKey(); + boolean isValid = verifySignature(blob, blobSignature, pk); + + return String.format( + "isValid=%b for message: %s\n\tsignature: %s\n\tpublic cert: %s", + isValid, + message, + Arrays.toString(blobSignature), + Arrays.toString(publicCert)); + } + // [END asserting_identity_to_other_services] + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setContentType("text/plain"); + try { + resp.getWriter().println(simulateIdentityAssertion()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortener.java b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortener.java new file mode 100644 index 00000000000..4ef26aa135a --- /dev/null +++ b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortener.java @@ -0,0 +1,80 @@ +/* + * Copyright 2016 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.example.appengine.appidentity; + +import com.google.appengine.api.appidentity.AppIdentityService; +import com.google.appengine.api.appidentity.AppIdentityServiceFactory; +import com.google.common.io.CharStreams; + +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +@SuppressWarnings("serial") +class UrlShortener { + // [START asserting_identity_to_Google_APIs] + /** + * Returns a shortened URL by calling the Google URL Shortener API. + * + *

Note: Error handling elided for simplicity. + */ + public String createShortUrl(String longUrl) throws Exception { + ArrayList scopes = new ArrayList(); + scopes.add("https://www.googleapis.com/auth/urlshortener"); + final AppIdentityService appIdentity = AppIdentityServiceFactory.getAppIdentityService(); + final AppIdentityService.GetAccessTokenResult accessToken = appIdentity.getAccessToken(scopes); + // The token asserts the identity reported by appIdentity.getServiceAccountName() + JSONObject request = new JSONObject(); + request.put("longUrl", longUrl); + + URL url = new URL("https://www.googleapis.com/urlshortener/v1/url?pp=1"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.addRequestProperty("Content-Type", "application/json"); + connection.addRequestProperty("Authorization", "Bearer " + accessToken.getAccessToken()); + + OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); + request.write(writer); + writer.close(); + + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + // Note: Should check the content-encoding. + // Any JSON parser can be used; this one is used for illustrative purposes. + JSONTokener responseTokens = new JSONTokener(connection.getInputStream()); + JSONObject response = new JSONObject(responseTokens); + return (String) response.get("id"); + } else { + try (InputStream s = connection.getErrorStream(); + InputStreamReader r = new InputStreamReader(s, StandardCharsets.UTF_8)) { + throw new RuntimeException(String.format( + "got error (%d) response %s from %s", + connection.getResponseCode(), + CharStreams.toString(r), + connection.toString())); + } + } + } + // [END asserting_identity_to_Google_APIs] +} diff --git a/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortenerServlet.java b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortenerServlet.java new file mode 100644 index 00000000000..324b8dcd0a1 --- /dev/null +++ b/appengine-java8/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortenerServlet.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016 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.example.appengine.appidentity; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@SuppressWarnings("serial") +public class UrlShortenerServlet extends HttpServlet { + private final UrlShortener shortener; + + public UrlShortenerServlet() { + shortener = new UrlShortener(); + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + PrintWriter writer = resp.getWriter(); + writer.println(""); + writer.println(""); + writer.println( + "Asserting Identity to Google APIs - App Engine App Identity Example"); + writer.println("

"); + writer.println(""); + writer.println(""); + writer.println(""); + writer.println("
"); + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setContentType("text/plain"); + String longUrl = req.getParameter("longUrl"); + if (longUrl == null) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "missing longUrl parameter"); + return; + } + + String shortUrl; + PrintWriter writer = resp.getWriter(); + try { + shortUrl = shortener.createShortUrl(longUrl); + } catch (Exception e) { + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + writer.println("error shortening URL: " + longUrl); + e.printStackTrace(writer); + return; + } + + writer.print("long URL: "); + writer.println(longUrl); + writer.print("short URL: "); + writer.println(shortUrl); + } +} diff --git a/appengine-java8/appidentity/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/appidentity/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..1ddd6f6a2c1 --- /dev/null +++ b/appengine-java8/appidentity/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,5 @@ + + + true + java8 + diff --git a/appengine-java8/appidentity/src/main/webapp/WEB-INF/web.xml b/appengine-java8/appidentity/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..3296a0799f4 --- /dev/null +++ b/appengine-java8/appidentity/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,30 @@ + + + + appidentity + com.example.appengine.appidentity.IdentityServlet + + + signforapp + com.example.appengine.appidentity.SignForAppServlet + + + urlshortener + com.example.appengine.appidentity.UrlShortenerServlet + + + appidentity + / + + + signforapp + /sign + + + urlshortener + /shorten + + diff --git a/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java b/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java new file mode 100644 index 00000000000..74041b24cd3 --- /dev/null +++ b/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015 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.example.appengine.appidentity; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link IdentityServlet}. + */ +@RunWith(JUnit4.class) +public class IdentityServletTest { + + // Set up a helper so that the ApiProxy returns a valid environment for local testing. + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private IdentityServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new IdentityServlet(); + } + + @After public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_defaultEnvironment_writesResponse() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + // We don't have any guarantee over what the local App Engine environment returns for + // "com.google.appengine.runtime.default_version_hostname". Only assert that the response + // contains part of the string we have control over. + assertThat(responseWriter.toString()) + .named("IdentityServlet response") + .contains("default_version_hostname:"); + } +} diff --git a/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java b/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java new file mode 100644 index 00000000000..7cc1fa5411b --- /dev/null +++ b/appengine-java8/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016 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.example.appengine.appidentity; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link SignForAppServlet}. + */ +@RunWith(JUnit4.class) +public class SignForAppServletTest { + + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private SignForAppServlet servletUnderTest; + + @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new SignForAppServlet(); + } + + @After public void tearDown() { + helper.tearDown(); + } + + @Test public void doGet_defaultEnvironment_successfullyVerifiesSignature() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + assertThat(responseWriter.toString()) + .named("SignForAppServlet response") + .contains("isValid=true for message: abcdefg"); + } +} diff --git a/appengine-java8/cloudsql/README.md b/appengine-java8/cloudsql/README.md new file mode 100644 index 00000000000..def7251a81d --- /dev/null +++ b/appengine-java8/cloudsql/README.md @@ -0,0 +1,35 @@ +# Cloud SQL sample for Google App Engine +This sample demonstrates how to use [Cloud SQL](https://cloud.google.com/sql/) on Google App Engine + +## Setup +Before you can run or deploy the sample, you will need to create a [Cloud SQL instance)](https://cloud.google.com/sql/docs/create-instance) + +1. Create a new user and database for the application. The easiest way to do this is via the [Google +Developers Console](https://console.cloud.google.com/sql/instances). Alternatively, you can use MySQL tools such as the command line client or workbench. +2. Change the root password (under Access Control) and / or create a new user / password. +3. Create a Database (under Databases) (or use MySQL with `gcloud beta sql connect --user=root`) +4. Note the **Instance connection name** under Overview > properties +(It will look like project:instance for 1st Generation or project:region:zone for 2nd Generation) + +or + +```bash +gcloud sql instances describe | grep connectionName +``` + +## Deploying + +```bash +$ mvn clean appengine:deploy -DINSTANCE_CONNECTION_NAME=instanceConnectionName -Duser=root +-Dpassword=myPassword -Ddatabase=myDatabase +``` + +Or you can update the properties in `pom.xml` + +## Running locally + +```bash +$ mvn clean appengine:run -DINSTANCE_CONNECTION_NAME=instanceConnectionName -Duser=root -Dpassword=myPassowrd -Ddatabase=myDatabase +``` +Note - you must use a local mysql instance for the 1st Generation instance and change the local Url +in `src/main/webapp/WEB-INF/appengine-web.xml` to use your local server. diff --git a/appengine-java8/cloudsql/pom.xml b/appengine-java8/cloudsql/pom.xml new file mode 100644 index 00000000000..0147051f3a3 --- /dev/null +++ b/appengine-java8/cloudsql/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-cloudsql-j8 + + + appengine-java8-samples + com.google.cloud + 1.0.0 + .. + + + + + + + root + myPassword + sqldemo + + + 1.3.1 + 1.8 + 1.8 + + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + com.google.api-client + google-api-client-appengine + 1.22.0 + + + + + mysql + mysql-connector-java + 5.1.40 + + + com.google.cloud.sql + mysql-socket-factory + 1.0.2 + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + org.apache.maven.plugins + maven-war-plugin + 3.0.0 + + + + + ${basedir}/src/main/webapp/WEB-INF + true + WEB-INF + + + + + + + com.google.cloud.tools + appengine-maven-plugin + ${appengine.maven.plugin} + + true + true + + + + + + diff --git a/appengine-java8/cloudsql/src/main/java/com/example/appengine/cloudsql/CloudSqlServlet.java b/appengine-java8/cloudsql/src/main/java/com/example/appengine/cloudsql/CloudSqlServlet.java new file mode 100644 index 00000000000..9e09fbede8e --- /dev/null +++ b/appengine-java8/cloudsql/src/main/java/com/example/appengine/cloudsql/CloudSqlServlet.java @@ -0,0 +1,106 @@ +/** + * Copyright 2016 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.example.appengine.cloudsql; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Date; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class CloudSqlServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + String path = req.getRequestURI(); + if (path.startsWith("/favicon.ico")) { + return; // ignore the request for favicon.ico + } + // store only the first two octets of a users ip address + String userIp = req.getRemoteAddr(); + InetAddress address = InetAddress.getByName(userIp); + if (address instanceof Inet6Address) { + // nest indexOf calls to find the second occurrence of a character in a string + // an alternative is to use Apache Commons Lang: StringUtils.ordinalIndexOf() + userIp = userIp.substring(0, userIp.indexOf(":", userIp.indexOf(":") + 1)) + ":*:*:*:*:*:*"; + } else if (address instanceof Inet4Address) { + userIp = userIp.substring(0, userIp.indexOf(".", userIp.indexOf(".") + 1)) + ".*.*"; + } + + final String createTableSql = "CREATE TABLE IF NOT EXISTS visits ( visit_id INT NOT NULL " + + "AUTO_INCREMENT, user_ip VARCHAR(46) NOT NULL, timestamp DATETIME NOT NULL, " + + "PRIMARY KEY (visit_id) )"; + final String createVisitSql = "INSERT INTO visits (user_ip, timestamp) VALUES (?, ?)"; + final String selectSql = "SELECT user_ip, timestamp FROM visits ORDER BY timestamp DESC " + + "LIMIT 10"; + + PrintWriter out = resp.getWriter(); + resp.setContentType("text/plain"); + String url; + if (System + .getProperty("com.google.appengine.runtime.version").startsWith("Google App Engine/")) { + // Check the System properties to determine if we are running on appengine or not + // Google App Engine sets a few system properties that will reliably be present on a remote + // instance. + url = System.getProperty("ae-cloudsql.cloudsql-database-url"); + try { + // Load the class that provides the new "jdbc:google:mysql://" prefix. + Class.forName("com.mysql.jdbc.GoogleDriver"); + } catch (ClassNotFoundException e) { + throw new ServletException("Error loading Google JDBC Driver", e); + } + } else { + // Set the url with the local MySQL database connection url when running locally + url = System.getProperty("ae-cloudsql.local-database-url"); + } + log("connecting to: " + url); + try (Connection conn = DriverManager.getConnection(url); + PreparedStatement statementCreateVisit = conn.prepareStatement(createVisitSql)) { + conn.createStatement().executeUpdate(createTableSql); + statementCreateVisit.setString(1, userIp); + statementCreateVisit.setTimestamp(2, new Timestamp(new Date().getTime())); + statementCreateVisit.executeUpdate(); + + try (ResultSet rs = conn.prepareStatement(selectSql).executeQuery()) { + out.print("Last 10 visits:\n"); + while (rs.next()) { + String savedIp = rs.getString("user_ip"); + String timeStamp = rs.getString("timestamp"); + out.print("Time: " + timeStamp + " Addr: " + savedIp + "\n"); + } + } + } catch (SQLException e) { + throw new ServletException("SQL error", e); + } + } +} +// [END example] diff --git a/appengine-java8/cloudsql/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/cloudsql/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..f028552e364 --- /dev/null +++ b/appengine-java8/cloudsql/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,25 @@ + + + + + true + java8 + + true + + + + + + diff --git a/appengine-java8/cloudsql/src/main/webapp/WEB-INF/web.xml b/appengine-java8/cloudsql/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..2f5b0dfbd32 --- /dev/null +++ b/appengine-java8/cloudsql/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,28 @@ + + + + + + + cloudsql + com.example.appengine.cloudsql.CloudSqlServlet + + + cloudsql + / + + diff --git a/appengine-java8/datastore/README.md b/appengine-java8/datastore/README.md new file mode 100644 index 00000000000..4a9f625455a --- /dev/null +++ b/appengine-java8/datastore/README.md @@ -0,0 +1,28 @@ +# Google Cloud Datastore Sample + +This sample demonstrates how to use [Google Cloud Datastore][java-datastore] +from [Google App Engine standard environment][ae-docs]. + +[java-datastore]: https://cloud.google.com/appengine/docs/java/datastore/ +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + + +## Running locally + +This example uses the +[Cloud SDK Maven plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven). +To run this sample locally: + + $ mvn appengine:run + +To see the results of the sample application, open +[localhost:8080](http://localhost:8080) in a web browser. + + +## Deploying + +In the following command, replace YOUR-PROJECT-ID with your +[Google Cloud Project ID](https://developers.google.com/console/help/new/#projectnumber) +and SOME-VERSION with a valid version number. + + $ mvn appengine:deploy diff --git a/appengine-java8/datastore/indexes-exploding/pom.xml b/appengine-java8/datastore/indexes-exploding/pom.xml new file mode 100644 index 00000000000..c0a049533bb --- /dev/null +++ b/appengine-java8/datastore/indexes-exploding/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-datastore-indexes-exploding-j8 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + ../.. + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.32 + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/datastore/indexes-exploding/src/main/java/com/example/appengine/IndexesServlet.java b/appengine-java8/datastore/indexes-exploding/src/main/java/com/example/appengine/IndexesServlet.java new file mode 100644 index 00000000000..04d4fe018d4 --- /dev/null +++ b/appengine-java8/datastore/indexes-exploding/src/main/java/com/example/appengine/IndexesServlet.java @@ -0,0 +1,62 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.CompositeFilterOperator; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.datastore.Query.FilterPredicate; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * A servlet to demonstrate the use of Cloud Datastore indexes. + */ +public class IndexesServlet extends HttpServlet { + private final DatastoreService datastore; + + public IndexesServlet() { + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + Query q = + new Query("Widget") + .setFilter( + CompositeFilterOperator.and( + new FilterPredicate("x", FilterOperator.EQUAL, 1), + new FilterPredicate("y", FilterOperator.EQUAL, "red"))) + .addSort("date", Query.SortDirection.ASCENDING); + List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults()); + + PrintWriter out = resp.getWriter(); + out.printf("Got %d widgets.\n", results.size()); + } +} diff --git a/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..b27da1dd82a --- /dev/null +++ b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,20 @@ + + + + java8 + true + diff --git a/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/datastore-indexes.xml new file mode 100644 index 00000000000..f1f44c38f67 --- /dev/null +++ b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/datastore-indexes.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/web.xml b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..6af27edbd87 --- /dev/null +++ b/appengine-java8/datastore/indexes-exploding/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,39 @@ + + + + + indexes-servlet + com.example.appengine.IndexesServlet + + + indexes-servlet + / + + + + + profile + /* + + + CONFIDENTIAL + + + diff --git a/appengine-java8/datastore/indexes-exploding/src/test/java/com/example/appengine/IndexesServletTest.java b/appengine-java8/datastore/indexes-exploding/src/test/java/com/example/appengine/IndexesServletTest.java new file mode 100644 index 00000000000..ea86f530fef --- /dev/null +++ b/appengine-java8/datastore/indexes-exploding/src/test/java/com/example/appengine/IndexesServletTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Date; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link IndexesServlet}. + */ +@RunWith(JUnit4.class) +public class IndexesServletTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private IndexesServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new IndexesServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_emptyDatastore_writesNoWidgets() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + assertThat(responseWriter.toString()) + .named("IndexesServlet response") + .isEqualTo("Got 0 widgets.\n"); + } + + @Test + public void doGet_repeatedPropertyEntities_writesWidgets() throws Exception { + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + // [START exploding_index_example_3] + Entity widget = new Entity("Widget"); + widget.setProperty("x", Arrays.asList(1, 2, 3, 4)); + widget.setProperty("y", Arrays.asList("red", "green", "blue")); + widget.setProperty("date", new Date()); + datastore.put(widget); + // [END exploding_index_example_3] + + servletUnderTest.doGet(mockRequest, mockResponse); + + assertThat(responseWriter.toString()) + .named("IndexesServlet response") + .isEqualTo("Got 1 widgets.\n"); + } +} diff --git a/appengine-java8/datastore/indexes-perfect/pom.xml b/appengine-java8/datastore/indexes-perfect/pom.xml new file mode 100644 index 00000000000..1e8359e42df --- /dev/null +++ b/appengine-java8/datastore/indexes-perfect/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-datastore-indexes-perfect-j8 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + ../.. + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.32 + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/datastore/indexes-perfect/src/main/java/com/example/appengine/IndexesServlet.java b/appengine-java8/datastore/indexes-perfect/src/main/java/com/example/appengine/IndexesServlet.java new file mode 100644 index 00000000000..5a01f1a53e5 --- /dev/null +++ b/appengine-java8/datastore/indexes-perfect/src/main/java/com/example/appengine/IndexesServlet.java @@ -0,0 +1,98 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.CompositeFilterOperator; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.datastore.Query.FilterPredicate; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * A servlet to demonstrate the use of Cloud Datastore indexes. + */ +public class IndexesServlet extends HttpServlet { + private final DatastoreService datastore; + + public IndexesServlet() { + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + PrintWriter out = resp.getWriter(); + // These queries should all work with the same index. + // [START queries_and_indexes_example_1] + Query q1 = + new Query("Person") + .setFilter( + CompositeFilterOperator.and( + new FilterPredicate("lastName", FilterOperator.EQUAL, "Smith"), + new FilterPredicate("height", FilterOperator.EQUAL, 72))) + .addSort("height", Query.SortDirection.DESCENDING); + // [END queries_and_indexes_example_1] + List r1 = datastore.prepare(q1).asList(FetchOptions.Builder.withDefaults()); + out.printf("Got %d results from query 1.\n", r1.size()); + + // [START queries_and_indexes_example_2] + Query q2 = + new Query("Person") + .setFilter( + CompositeFilterOperator.and( + new FilterPredicate("lastName", FilterOperator.EQUAL, "Jones"), + new FilterPredicate("height", FilterOperator.EQUAL, 63))) + .addSort("height", Query.SortDirection.DESCENDING); + // [END queries_and_indexes_example_2] + List r2 = datastore.prepare(q2).asList(FetchOptions.Builder.withDefaults()); + out.printf("Got %d results from query 2.\n", r2.size()); + + // [START queries_and_indexes_example_3] + Query q3 = + new Query("Person") + .setFilter( + CompositeFilterOperator.and( + new FilterPredicate("lastName", FilterOperator.EQUAL, "Friedkin"), + new FilterPredicate("firstName", FilterOperator.EQUAL, "Damian"))) + .addSort("height", Query.SortDirection.ASCENDING); + // [END queries_and_indexes_example_3] + List r3 = datastore.prepare(q3).asList(FetchOptions.Builder.withDefaults()); + out.printf("Got %d results from query 3.\n", r3.size()); + + // [START queries_and_indexes_example_4] + Query q4 = + new Query("Person") + .setFilter(new FilterPredicate("lastName", FilterOperator.EQUAL, "Blair")) + .addSort("firstName", Query.SortDirection.ASCENDING) + .addSort("height", Query.SortDirection.ASCENDING); + // [END queries_and_indexes_example_4] + List r4 = datastore.prepare(q4).asList(FetchOptions.Builder.withDefaults()); + out.printf("Got %d results from query 4.\n", r4.size()); + } +} diff --git a/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..b27da1dd82a --- /dev/null +++ b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,20 @@ + + + + java8 + true + diff --git a/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/datastore-indexes.xml new file mode 100644 index 00000000000..30d97392eaa --- /dev/null +++ b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/datastore-indexes.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/web.xml b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..6af27edbd87 --- /dev/null +++ b/appengine-java8/datastore/indexes-perfect/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,39 @@ + + + + + indexes-servlet + com.example.appengine.IndexesServlet + + + indexes-servlet + / + + + + + profile + /* + + + CONFIDENTIAL + + + diff --git a/appengine-java8/datastore/indexes-perfect/src/test/java/com/example/appengine/IndexesServletTest.java b/appengine-java8/datastore/indexes-perfect/src/test/java/com/example/appengine/IndexesServletTest.java new file mode 100644 index 00000000000..d1816a7034d --- /dev/null +++ b/appengine-java8/datastore/indexes-perfect/src/test/java/com/example/appengine/IndexesServletTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link IndexesServlet}. + */ +@RunWith(JUnit4.class) +public class IndexesServletTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private IndexesServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new IndexesServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_emptyDatastore_writesNoWidgets() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + String response = responseWriter.toString(); + assertThat(response).contains("Got 0 results from query 1."); + assertThat(response).contains("Got 0 results from query 2."); + assertThat(response).contains("Got 0 results from query 3."); + assertThat(response).contains("Got 0 results from query 4."); + } +} diff --git a/appengine-java8/datastore/indexes/pom.xml b/appengine-java8/datastore/indexes/pom.xml new file mode 100644 index 00000000000..86d5cc7b8c2 --- /dev/null +++ b/appengine-java8/datastore/indexes/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-datastore-indexes-j8 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + ../.. + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.32 + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/datastore/indexes/src/main/java/com/example/appengine/IndexesServlet.java b/appengine-java8/datastore/indexes/src/main/java/com/example/appengine/IndexesServlet.java new file mode 100644 index 00000000000..5493ce1f1a9 --- /dev/null +++ b/appengine-java8/datastore/indexes/src/main/java/com/example/appengine/IndexesServlet.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.CompositeFilterOperator; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.datastore.Query.FilterPredicate; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * A servlet to demonstrate the use of Cloud Datastore indexes. + */ +public class IndexesServlet extends HttpServlet { + private final DatastoreService datastore; + + public IndexesServlet() { + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + // [START exploding_index_example_1] + Query q = + new Query("Widget") + .setFilter( + CompositeFilterOperator.and( + new FilterPredicate("x", FilterOperator.EQUAL, 1), + new FilterPredicate("y", FilterOperator.EQUAL, 2))) + .addSort("date", Query.SortDirection.ASCENDING); + // [END exploding_index_example_1] + List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults()); + + PrintWriter out = resp.getWriter(); + out.printf("Got %d widgets.\n", results.size()); + } +} diff --git a/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..b27da1dd82a --- /dev/null +++ b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,20 @@ + + + + java8 + true + diff --git a/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/datastore-indexes.xml new file mode 100644 index 00000000000..28046fb9571 --- /dev/null +++ b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/datastore-indexes.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/web.xml b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..6af27edbd87 --- /dev/null +++ b/appengine-java8/datastore/indexes/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,39 @@ + + + + + indexes-servlet + com.example.appengine.IndexesServlet + + + indexes-servlet + / + + + + + profile + /* + + + CONFIDENTIAL + + + diff --git a/appengine-java8/datastore/indexes/src/test/java/com/example/appengine/IndexesServletTest.java b/appengine-java8/datastore/indexes/src/test/java/com/example/appengine/IndexesServletTest.java new file mode 100644 index 00000000000..83575239136 --- /dev/null +++ b/appengine-java8/datastore/indexes/src/test/java/com/example/appengine/IndexesServletTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link IndexesServlet}. + */ +@RunWith(JUnit4.class) +public class IndexesServletTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig()); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private IndexesServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new IndexesServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_emptyDatastore_writesNoWidgets() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + assertThat(responseWriter.toString()) + .named("IndexesServlet response") + .isEqualTo("Got 0 widgets.\n"); + } +} diff --git a/appengine-java8/datastore/pom.xml b/appengine-java8/datastore/pom.xml new file mode 100644 index 00000000000..22cb53265a0 --- /dev/null +++ b/appengine-java8/datastore/pom.xml @@ -0,0 +1,125 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-datastore-j8 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + com.google.auto.value + auto-value + 1.4.1 + provided + + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + + com.google.guava + guava + 20.0 + + + + joda-time + joda-time + 2.9.9 + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.32 + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java b/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java new file mode 100644 index 00000000000..b87edf7eeba --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbook.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.example.time.Clock; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; +import com.google.common.collect.ImmutableList; + +import java.util.Date; +import java.util.List; + +/** + * A log of notes left by users. + * + *

This is meant to be subclassed to demonstrate different storage structures in Datastore. + */ +abstract class AbstractGuestbook { + private final DatastoreService datastore; + private final UserService userService; + private final Clock clock; + + AbstractGuestbook(Clock clock) { + this.datastore = DatastoreServiceFactory.getDatastoreService(); + this.userService = UserServiceFactory.getUserService(); + this.clock = clock; + } + + /** + * Appends a new greeting to the guestbook and returns the {@link Entity} that was created. + */ + public Greeting appendGreeting(String content) { + Greeting greeting = + Greeting.create( + createGreeting( + datastore, + userService.getCurrentUser(), + clock.now().toDate(), + content)); + return greeting; + } + + /** + * Write a greeting to Datastore. + */ + protected abstract Entity createGreeting( + DatastoreService datastore, User user, Date date, String content); + + /** + * Return a list of the most recent greetings. + */ + public List listGreetings() { + ImmutableList.Builder greetings = ImmutableList.builder(); + for (Entity entity : listGreetingEntities(datastore)) { + greetings.add(Greeting.create(entity)); + } + return greetings.build(); + } + + /** + * Return a list of the most recent greetings. + */ + protected abstract List listGreetingEntities(DatastoreService datastore); +} diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java new file mode 100644 index 00000000000..e77231497c6 --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/appengine/AbstractGuestbookServlet.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016 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.example.appengine; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +abstract class AbstractGuestbookServlet extends HttpServlet { + private final AbstractGuestbook guestbook; + + public AbstractGuestbookServlet(AbstractGuestbook guestbook) { + this.guestbook = guestbook; + } + + private void renderGuestbook(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + resp.setContentType("text/html"); + resp.setCharacterEncoding("UTF-8"); + req.setAttribute("greetings", guestbook.listGreetings()); + req.getRequestDispatcher("/guestbook.jsp").forward(req, resp); + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + renderGuestbook(req, resp); + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + String content = req.getParameter("content"); + if (content == null || content.isEmpty()) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "missing content"); + return; + } + guestbook.appendGreeting(content); + renderGuestbook(req, resp); + } +} + diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/Greeting.java b/appengine-java8/datastore/src/main/java/com/example/appengine/Greeting.java new file mode 100644 index 00000000000..352aa0a1dde --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/appengine/Greeting.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.users.User; +import com.google.auto.value.AutoValue; +import org.joda.time.Instant; + +import java.util.Date; + +import javax.annotation.Nullable; + +@AutoValue +public abstract class Greeting { + static Greeting create(Entity entity) { + User user = (User) entity.getProperty("user"); + Instant date = new Instant((Date) entity.getProperty("date")); + String content = (String) entity.getProperty("content"); + return new AutoValue_Greeting(user, date, content); + } + + @Nullable + public abstract User getUser(); + + public abstract Instant getDate(); + + public abstract String getContent(); +} diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/Guestbook.java b/appengine-java8/datastore/src/main/java/com/example/appengine/Guestbook.java new file mode 100644 index 00000000000..83e984818c4 --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/appengine/Guestbook.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.example.time.Clock; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.users.User; + +import java.util.Date; +import java.util.List; + +/** + * A log of notes left by users. + * + *

This demonstrates the use of Google Cloud Datastore using the App Engine + * APIs. See the + * documentation + * for more information. + */ +class Guestbook extends AbstractGuestbook { + Guestbook(Clock clock) { + super(clock); + } + + @Override + protected Entity createGreeting( + DatastoreService datastore, User user, Date date, String content) { + // No parent key specified, so Greeting is a root entity. + Entity greeting = new Entity("Greeting"); + greeting.setProperty("user", user); + greeting.setProperty("date", date); + greeting.setProperty("content", content); + + datastore.put(greeting); + return greeting; + } + + @Override + protected List listGreetingEntities(DatastoreService datastore) { + Query query = + new Query("Greeting") + .addSort("date", Query.SortDirection.DESCENDING); + return datastore.prepare(query) + .asList(FetchOptions.Builder.withLimit(10)); + } + + +} diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookServlet.java new file mode 100644 index 00000000000..5ebab796530 --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookServlet.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.example.time.SystemClock; + +public class GuestbookServlet extends AbstractGuestbookServlet { + public GuestbookServlet() { + super(new Guestbook(new SystemClock())); + } +} diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrong.java b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrong.java new file mode 100644 index 00000000000..5f3ad23b91d --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrong.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.example.time.Clock; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.users.User; + +import java.util.Date; +import java.util.List; + +/** + * A log of notes left by users. + * + *

This demonstrates the use of Google Cloud Datastore using the App Engine + * APIs. See the + * documentation + * for more information. + */ +class GuestbookStrong extends AbstractGuestbook { + private final String guestbookName; + + GuestbookStrong(String guestbookName, Clock clock) { + super(clock); + this.guestbookName = guestbookName; + } + + @Override + protected Entity createGreeting( + DatastoreService datastore, User user, Date date, String content) { + // String guestbookName = "my guestbook"; -- Set elsewhere (injected to the constructor). + Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName); + + // Place greeting in the same entity group as guestbook. + Entity greeting = new Entity("Greeting", guestbookKey); + greeting.setProperty("user", user); + greeting.setProperty("date", date); + greeting.setProperty("content", content); + + datastore.put(greeting); + return greeting; + } + + @Override + protected List listGreetingEntities(DatastoreService datastore) { + Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName); + Query query = + new Query("Greeting", guestbookKey) + .setAncestor(guestbookKey) + .addSort("date", Query.SortDirection.DESCENDING); + return datastore.prepare(query) + .asList(FetchOptions.Builder.withLimit(10)); + } +} diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java new file mode 100644 index 00000000000..e73ae1e0fcc --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/appengine/GuestbookStrongServlet.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.example.time.SystemClock; + +public class GuestbookStrongServlet extends AbstractGuestbookServlet { + public static final String GUESTBOOK_ID = "my guestbook"; + + public GuestbookStrongServlet() { + super(new GuestbookStrong(GUESTBOOK_ID, new SystemClock())); + } +} diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java new file mode 100644 index 00000000000..b59aaab3aa0 --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/appengine/ListPeopleServlet.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016 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.example.appengine; + +// [START cursors] +import com.google.appengine.api.datastore.Cursor; +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.SortDirection; +import com.google.appengine.api.datastore.QueryResultList; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class ListPeopleServlet extends HttpServlet { + static final int PAGE_SIZE = 15; + private final DatastoreService datastore; + + public ListPeopleServlet() { + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + FetchOptions fetchOptions = FetchOptions.Builder.withLimit(PAGE_SIZE); + + // If this servlet is passed a cursor parameter, let's use it. + String startCursor = req.getParameter("cursor"); + if (startCursor != null) { + fetchOptions.startCursor(Cursor.fromWebSafeString(startCursor)); + } + + Query q = new Query("Person").addSort("name", SortDirection.ASCENDING); + PreparedQuery pq = datastore.prepare(q); + + QueryResultList results; + try { + results = pq.asQueryResultList(fetchOptions); + } catch (IllegalArgumentException e) { + // IllegalArgumentException happens when an invalid cursor is used. + // A user could have manually entered a bad cursor in the URL or there + // may have been an internal implementation detail change in App Engine. + // Redirect to the page without the cursor parameter to show something + // rather than an error. + resp.sendRedirect("/people"); + return; + } + + resp.setContentType("text/html"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter w = resp.getWriter(); + w.println(""); + w.println(""); + w.println("Cloud Datastore Cursor Sample"); + w.println("

    "); + for (Entity entity : results) { + w.println("
  • " + entity.getProperty("name") + "
  • "); + } + w.println("
"); + + String cursorString = results.getCursor().toWebSafeString(); + + // This servlet lives at '/people'. + w.println("Next page"); + } +} +// [END cursors] diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/ProjectionServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/ProjectionServlet.java new file mode 100644 index 00000000000..81bdaf6cf9b --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/appengine/ProjectionServlet.java @@ -0,0 +1,79 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PropertyProjection; +import com.google.appengine.api.datastore.Query; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Date; +import java.util.List; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Servlet to demonstrate use of Datastore projection queries. + * + *

See the + * documentation + * for using Datastore projection queries from the Google App Engine standard environment. + */ +@SuppressWarnings("serial") +public class ProjectionServlet extends HttpServlet { + private static final String GUESTBOOK_ID = GuestbookStrongServlet.GUESTBOOK_ID; + private final DatastoreService datastore; + + public ProjectionServlet() { + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter out = resp.getWriter(); + out.printf("Latest entries from guestbook: \n"); + + Key guestbookKey = KeyFactory.createKey("Guestbook", GUESTBOOK_ID); + Query query = new Query("Greeting", guestbookKey); + addGuestbookProjections(query); + printGuestbookEntries(datastore, query, out); + } + + private void addGuestbookProjections(Query query) { + query.addProjection(new PropertyProjection("content", String.class)); + query.addProjection(new PropertyProjection("date", Date.class)); + } + + private void printGuestbookEntries(DatastoreService datastore, Query query, PrintWriter out) { + List guests = datastore.prepare(query).asList(FetchOptions.Builder.withLimit(5)); + for (Entity guest : guests) { + String content = (String) guest.getProperty("content"); + Date stamp = (Date) guest.getProperty("date"); + out.printf("Message %s posted on %s.\n", content, stamp.toString()); + } + } +} diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/StartupServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/StartupServlet.java new file mode 100644 index 00000000000..2c523f3db15 --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/appengine/StartupServlet.java @@ -0,0 +1,120 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.EntityNotFoundException; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.common.collect.ImmutableList; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * A startup handler to populate the datastore with example entities. + */ +public class StartupServlet extends HttpServlet { + static final String IS_POPULATED_ENTITY = "IsPopulated"; + static final String IS_POPULATED_KEY_NAME = "is-populated"; + + private static final String PERSON_ENTITY = "Person"; + private static final String NAME_PROPERTY = "name"; + private static final ImmutableList US_PRESIDENTS = + ImmutableList.builder() + .add("George Washington") + .add("John Adams") + .add("Thomas Jefferson") + .add("James Madison") + .add("James Monroe") + .add("John Quincy Adams") + .add("Andrew Jackson") + .add("Martin Van Buren") + .add("William Henry Harrison") + .add("John Tyler") + .add("James K. Polk") + .add("Zachary Taylor") + .add("Millard Fillmore") + .add("Franklin Pierce") + .add("James Buchanan") + .add("Abraham Lincoln") + .add("Andrew Johnson") + .add("Ulysses S. Grant") + .add("Rutherford B. Hayes") + .add("James A. Garfield") + .add("Chester A. Arthur") + .add("Grover Cleveland") + .add("Benjamin Harrison") + .add("Grover Cleveland") + .add("William McKinley") + .add("Theodore Roosevelt") + .add("William Howard Taft") + .add("Woodrow Wilson") + .add("Warren G. Harding") + .add("Calvin Coolidge") + .add("Herbert Hoover") + .add("Franklin D. Roosevelt") + .add("Harry S. Truman") + .add("Dwight D. Eisenhower") + .add("John F. Kennedy") + .add("Lyndon B. Johnson") + .add("Richard Nixon") + .add("Gerald Ford") + .add("Jimmy Carter") + .add("Ronald Reagan") + .add("George H. W. Bush") + .add("Bill Clinton") + .add("George W. Bush") + .add("Barack Obama") + .build(); + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + + Key isPopulatedKey = KeyFactory.createKey(IS_POPULATED_ENTITY, IS_POPULATED_KEY_NAME); + boolean isAlreadyPopulated; + try { + datastore.get(isPopulatedKey); + isAlreadyPopulated = true; + } catch (EntityNotFoundException expected) { + isAlreadyPopulated = false; + } + if (isAlreadyPopulated) { + resp.getWriter().println("ok"); + return; + } + + ImmutableList.Builder people = ImmutableList.builder(); + for (String name : US_PRESIDENTS) { + Entity person = new Entity(PERSON_ENTITY); + person.setProperty(NAME_PROPERTY, name); + people.add(person); + } + datastore.put(people.build()); + datastore.put(new Entity(isPopulatedKey)); + resp.getWriter().println("ok"); + } +} diff --git a/appengine-java8/datastore/src/main/java/com/example/appengine/StatsServlet.java b/appengine-java8/datastore/src/main/java/com/example/appengine/StatsServlet.java new file mode 100644 index 00000000000..b6d6925fc12 --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/appengine/StatsServlet.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.Query; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class StatsServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // [START stat_example] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + Entity globalStat = datastore.prepare(new Query("__Stat_Total__")).asSingleEntity(); + Long totalBytes = (Long) globalStat.getProperty("bytes"); + Long totalEntities = (Long) globalStat.getProperty("count"); + // [END stat_example] + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter w = resp.getWriter(); + w.printf("%d bytes\n%d entities\n", totalBytes, totalEntities); + } +} +// [END cursors] diff --git a/appengine-java8/datastore/src/main/java/com/example/time/Clock.java b/appengine-java8/datastore/src/main/java/com/example/time/Clock.java new file mode 100644 index 00000000000..7d75e2b62d4 --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/time/Clock.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016 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.example.time; + +import org.joda.time.Instant; + +/** + * Provides the current value of "now." To preserve testability, avoid all other libraries that + * access the system clock (whether {@linkplain System#currentTimeMillis directly} or {@linkplain + * org.joda.time.DateTime#DateTime() indirectly}). + * + *

In production, use the {@link SystemClock} implementation to return the "real" system time. In + * tests, either use {@link com.example.time.testing.FakeClock}, or get an instance from a mocking + * framework such as Mockito. + */ +public interface Clock { + /** + * Returns the current, absolute time according to this clock. + */ + Instant now(); +} diff --git a/appengine-java8/datastore/src/main/java/com/example/time/SystemClock.java b/appengine-java8/datastore/src/main/java/com/example/time/SystemClock.java new file mode 100644 index 00000000000..032230bd0c4 --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/time/SystemClock.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 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.example.time; + +import org.joda.time.Instant; + +/** + * Clock implementation that returns the "real" system time. + * + *

This class exists so that we can use a fake implementation for unit + * testing classes that need the current time value. See {@link Clock} for + * general information about clocks. + */ +public class SystemClock implements Clock { + /** + * Creates a new instance. All {@code SystemClock} instances function identically. + */ + public SystemClock() {} + + @Override + public Instant now() { + return new Instant(); + } +} diff --git a/appengine-java8/datastore/src/main/java/com/example/time/testing/FakeClock.java b/appengine-java8/datastore/src/main/java/com/example/time/testing/FakeClock.java new file mode 100644 index 00000000000..7ede635d762 --- /dev/null +++ b/appengine-java8/datastore/src/main/java/com/example/time/testing/FakeClock.java @@ -0,0 +1,186 @@ +/* + * Copyright 2016 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.example.time.testing; + +import com.example.time.Clock; + +import org.joda.time.Instant; +import org.joda.time.ReadableDuration; +import org.joda.time.ReadableInstant; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * A Clock that returns a fixed Instant value as the current clock time. The + * fixed Instant is settable for testing. Test code should hold a reference to + * the FakeClock, while code under test should hold a Clock reference. + * + *

The clock time can be incremented/decremented manually, with + * {@link #incrementTime} and {@link #decrementTime} respectively. + * + *

The clock can also be configured so that the time is incremented whenever + * {@link #now()} is called: see {@link #setAutoIncrementStep}. + */ +public class FakeClock implements Clock { + private static final Instant DEFAULT_TIME = new Instant(1000000000L); + private final long baseTimeMs; + private final AtomicLong fakeNowMs; + private volatile long autoIncrementStepMs; + + /** + * Creates a FakeClock instance initialized to an arbitrary constant. + */ + public FakeClock() { + this(DEFAULT_TIME); + } + + /** + * Creates a FakeClock instance initialized to the given time. + */ + public FakeClock(ReadableInstant now) { + baseTimeMs = now.getMillis(); + fakeNowMs = new AtomicLong(baseTimeMs); + } + + /** + * Sets the value of the underlying instance for testing purposes. + * + * @return this + */ + public FakeClock setNow(ReadableInstant now) { + fakeNowMs.set(now.getMillis()); + return this; + } + + @Override + public Instant now() { + return getAndAdd(autoIncrementStepMs); + } + + /** + * Returns the current time without applying an auto increment, if configured. + * The default behavior of {@link #now()} is the same as this method. + */ + public Instant peek() { + return new Instant(fakeNowMs.get()); + } + + /** + * Reset the given clock back to the base time with which the FakeClock was + * initially constructed. + * + * @return this + */ + public FakeClock resetTime() { + fakeNowMs.set(baseTimeMs); + return this; + } + + /** + * Increments the clock time by the given duration. + * + * @param duration the duration to increment the clock time by + * @return this + */ + public FakeClock incrementTime(ReadableDuration duration) { + incrementTime(duration.getMillis()); + return this; + } + + /** + * Increments the clock time by the given duration. + * + * @param durationMs the duration to increment the clock time by, + * in milliseconds + * @return this + */ + public FakeClock incrementTime(long durationMs) { + fakeNowMs.addAndGet(durationMs); + return this; + } + + /** + * Decrements the clock time by the given duration. + * + * @param duration the duration to decrement the clock time by + * @return this + */ + public FakeClock decrementTime(ReadableDuration duration) { + incrementTime(-duration.getMillis()); + return this; + } + + /** + * Decrements the clock time by the given duration. + * + * @param durationMs the duration to decrement the clock time by, + * in milliseconds + * @return this + */ + public FakeClock decrementTime(long durationMs) { + incrementTime(-durationMs); + return this; + } + + /** + * Sets the increment applied to the clock whenever it is queried. + * The increment is zero by default: the clock is left unchanged when queried. + * + * @param autoIncrementStep the new auto increment duration + * @return this + */ + public FakeClock setAutoIncrementStep(ReadableDuration autoIncrementStep) { + setAutoIncrementStep(autoIncrementStep.getMillis()); + return this; + } + + /** + * Sets the increment applied to the clock whenever it is queried. + * The increment is zero by default: the clock is left unchanged when queried. + * + * @param autoIncrementStepMs the new auto increment duration, in milliseconds + * @return this + */ + public FakeClock setAutoIncrementStep(long autoIncrementStepMs) { + this.autoIncrementStepMs = autoIncrementStepMs; + return this; + } + + /** + * Atomically adds the given value to the current time. + * + * @see AtomicLong#addAndGet + * + * @param durationMs the duration to add, in milliseconds + * @return the updated current time + */ + protected final Instant addAndGet(long durationMs) { + return new Instant(fakeNowMs.addAndGet(durationMs)); + } + + /** + * Atomically adds the given value to the current time. + * + * @see AtomicLong#getAndAdd + * + * @param durationMs the duration to add, in milliseconds + * @return the previous time + */ + protected final Instant getAndAdd(long durationMs) { + return new Instant(fakeNowMs.getAndAdd(durationMs)); + } +} diff --git a/appengine-java8/datastore/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/datastore/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..5d2d5ad2429 --- /dev/null +++ b/appengine-java8/datastore/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,19 @@ + + + + + + true + java8 + diff --git a/appengine-java8/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine-java8/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml new file mode 100644 index 00000000000..93f9a3d76cc --- /dev/null +++ b/appengine-java8/datastore/src/main/webapp/WEB-INF/datastore-indexes.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/appengine-java8/datastore/src/main/webapp/WEB-INF/web.xml b/appengine-java8/datastore/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..cf06f60d61e --- /dev/null +++ b/appengine-java8/datastore/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,97 @@ + + + + + guestbook-strong + com.example.appengine.GuestbookStrongServlet + + + guestbook-strong + / + + + guestbook + com.example.appengine.GuestbookServlet + + + guestbook + /guestbook + + + people + com.example.appengine.ListPeopleServlet + + + people + /people + + + projection + com.example.appengine.ProjectionServlet + + + projection + /projection + + + stats + com.example.appengine.StatsServlet + + + stats + /stats + + + + + startup + com.example.appengine.StartupServlet + + + startup + /_ah/start + + + + + profile + /* + + + CONFIDENTIAL + + + * + + + + + + profile + /stats + + + CONFIDENTIAL + + + admin + + + diff --git a/appengine-java8/datastore/src/main/webapp/guestbook.jsp b/appengine-java8/datastore/src/main/webapp/guestbook.jsp new file mode 100644 index 00000000000..4b65c8a308e --- /dev/null +++ b/appengine-java8/datastore/src/main/webapp/guestbook.jsp @@ -0,0 +1,45 @@ + + +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> + + + + Guestbook + + +

Latest Greetings

+ +

+ ${greeting.content}
+ Posted: ${greeting.date} +

+
+ +

Add Greeting

+
+

+ + +

+

+ +

+
+ + + diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/EntitiesTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/EntitiesTest.java new file mode 100644 index 00000000000..c9b7640efe4 --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/EntitiesTest.java @@ -0,0 +1,364 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.EmbeddedEntity; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.EntityNotFoundException; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.KeyRange; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Unit tests to demonstrate App Engine Datastore entities. + */ +@RunWith(JUnit4.class) +public class EntitiesTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + private DatastoreService datastore; + + @Before + public void setUp() { + helper.setUp(); + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void kindExample_writesEntity() throws Exception { + // [START kind_example] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + + Entity employee = new Entity("Employee", "asalieri"); + employee.setProperty("firstName", "Antonio"); + employee.setProperty("lastName", "Salieri"); + employee.setProperty("hireDate", new Date()); + employee.setProperty("attendedHrTraining", true); + + datastore.put(employee); + // [END kind_example] + + Entity got = datastore.get(employee.getKey()); + assertThat((String) got.getProperty("firstName")).named("got.firstName").isEqualTo("Antonio"); + assertThat((String) got.getProperty("lastName")).named("got.lastName").isEqualTo("Salieri"); + assertThat((Date) got.getProperty("hireDate")).named("got.hireDate").isNotNull(); + assertThat((boolean) got.getProperty("attendedHrTraining")) + .named("got.attendedHrTraining") + .isTrue(); + } + + @Test + public void identifiers_keyName_setsKeyName() throws Exception { + // [START identifiers_1] + Entity employee = new Entity("Employee", "asalieri"); + // [END identifiers_1] + datastore.put(employee); + + assertThat(employee.getKey().getName()).named("key name").isEqualTo("asalieri"); + } + + @Test + public void identifiers_autoId_setsUnallocatedId() throws Exception { + KeyRange keys = datastore.allocateIds("Employee", 1); + long usedId = keys.getStart().getId(); + + // [START identifiers_2] + Entity employee = new Entity("Employee"); + // [END identifiers_2] + datastore.put(employee); + + assertThat(employee.getKey().getId()).named("key id").isNotEqualTo(usedId); + } + + @Test + public void parent_withinEntityConstructor_setsParent() throws Exception { + // [START parent_1] + Entity employee = new Entity("Employee"); + datastore.put(employee); + + Entity address = new Entity("Address", employee.getKey()); + datastore.put(address); + // [END parent_1] + + assertThat(address.getParent()).named("address parent").isEqualTo(employee.getKey()); + } + + @Test + public void parent_withKeyName_setsKeyName() throws Exception { + Entity employee = new Entity("Employee"); + datastore.put(employee); + + // [START parent_2] + Entity address = new Entity("Address", "addr1", employee.getKey()); + // [END parent_2] + datastore.put(address); + + assertThat(address.getKey().getName()).named("address key name").isEqualTo("addr1"); + } + + @Test + public void datastoreServiceFactory_returnsDatastoreService() throws Exception { + // [START working_with_entities] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + // [END working_with_entities] + assertThat(datastore).named("datastore").isNotNull(); + } + + @Test + public void creatingAnEntity_withKeyName_writesEntity() throws Exception { + // [START creating_an_entity_1] + Entity employee = new Entity("Employee", "asalieri"); + // Set the entity properties. + // ... + datastore.put(employee); + // [END creating_an_entity_1] + + assertThat(employee.getKey().getName()).named("employee key name").isEqualTo("asalieri"); + } + + private Key writeEmptyEmployee() { + // [START creating_an_entity_2] + Entity employee = new Entity("Employee"); + // Set the entity properties. + // ... + datastore.put(employee); + // [END creating_an_entity_2] + return employee.getKey(); + } + + @Test + public void creatingAnEntity_withoutKeyName_writesEntity() throws Exception { + Key employeeKey = writeEmptyEmployee(); + // [START retrieving_an_entity] + // Key employeeKey = ...; + Entity employee = datastore.get(employeeKey); + // [END retrieving_an_entity] + + assertThat(employee.getKey().getId()).named("retrieved key ID").isEqualTo(employeeKey.getId()); + } + + @Test + public void deletingAnEntity_deletesAnEntity() throws Exception { + Entity employee = new Entity("Employee", "asalieri"); + datastore.put(employee); + + Key employeeKey = KeyFactory.createKey("Employee", "asalieri"); + // [START deleting_an_entity] + // Key employeeKey = ...; + datastore.delete(employeeKey); + // [END deleting_an_entity] + + try { + Entity got = datastore.get(employeeKey); + fail("Expected EntityNotFoundException"); + } catch (EntityNotFoundException expected) { + assertThat(expected.getKey().getName()).named("exception key name").isEqualTo("asalieri"); + } + } + + @Test + public void repeatedProperties_storesList() throws Exception { + // [START repeated_properties] + Entity employee = new Entity("Employee"); + ArrayList favoriteFruit = new ArrayList(); + favoriteFruit.add("Pear"); + favoriteFruit.add("Apple"); + employee.setProperty("favoriteFruit", favoriteFruit); + datastore.put(employee); + + // Sometime later + employee = datastore.get(employee.getKey()); + @SuppressWarnings("unchecked") // Cast can't verify generic type. + ArrayList retrievedFruits = (ArrayList) employee.getProperty("favoriteFruit"); + // [END repeated_properties] + + assertThat(retrievedFruits).containsExactlyElementsIn(favoriteFruit).inOrder(); + } + + @Test + public void embeddedEntity_fromEmbedded_embedsProperties() throws Exception { + Entity employee = new Entity("Employee"); + // [START embedded_entities_1] + // Entity employee = ...; + EmbeddedEntity embeddedContactInfo = new EmbeddedEntity(); + + embeddedContactInfo.setProperty("homeAddress", "123 Fake St, Made, UP 45678"); + embeddedContactInfo.setProperty("phoneNumber", "555-555-5555"); + embeddedContactInfo.setProperty("emailAddress", "test@example.com"); + + employee.setProperty("contactInfo", embeddedContactInfo); + // [END embedded_entities_1] + datastore.put(employee); + + Entity gotEmployee = datastore.get(employee.getKey()); + EmbeddedEntity got = (EmbeddedEntity) gotEmployee.getProperty("contactInfo"); + assertThat((String) got.getProperty("homeAddress")) + .named("got.homeAddress") + .isEqualTo("123 Fake St, Made, UP 45678"); + } + + private Key putEmployeeWithContactInfo(Entity contactInfo) { + Entity employee = new Entity("Employee"); + // [START embedded_entities_2] + // Entity employee = ...; + // Entity contactInfo = ...; + EmbeddedEntity embeddedContactInfo = new EmbeddedEntity(); + + embeddedContactInfo.setKey(contactInfo.getKey()); // Optional, used so we can recover original. + embeddedContactInfo.setPropertiesFrom(contactInfo); + + employee.setProperty("contactInfo", embeddedContactInfo); + // [END embedded_entities_2] + datastore.put(employee); + return employee.getKey(); + } + + @Test + public void embeddedEntity_fromExisting_canRecover() throws Exception { + Entity initialContactInfo = new Entity("Contact"); + initialContactInfo.setProperty("homeAddress", "123 Fake St, Made, UP 45678"); + initialContactInfo.setProperty("phoneNumber", "555-555-5555"); + initialContactInfo.setProperty("emailAddress", "test@example.com"); + datastore.put(initialContactInfo); + Key employeeKey = putEmployeeWithContactInfo(initialContactInfo); + + // [START embedded_entities_3] + Entity employee = datastore.get(employeeKey); + EmbeddedEntity embeddedContactInfo = (EmbeddedEntity) employee.getProperty("contactInfo"); + + Key infoKey = embeddedContactInfo.getKey(); + Entity contactInfo = new Entity(infoKey); + contactInfo.setPropertiesFrom(embeddedContactInfo); + // [END embedded_entities_3] + datastore.put(contactInfo); + + Entity got = datastore.get(infoKey); + assertThat(got.getKey()).isEqualTo(initialContactInfo.getKey()); + assertThat((String) got.getProperty("homeAddress")) + .named("got.homeAddress") + .isEqualTo("123 Fake St, Made, UP 45678"); + } + + @Test + public void batchOperations_putsEntities() { + // [START batch_operations] + Entity employee1 = new Entity("Employee"); + Entity employee2 = new Entity("Employee"); + Entity employee3 = new Entity("Employee"); + // [START_EXCLUDE] + employee1.setProperty("firstName", "Bill"); + employee2.setProperty("firstName", "Jane"); + employee3.setProperty("firstName", "Alex"); + // [END_EXCLUDE] + + List employees = Arrays.asList(employee1, employee2, employee3); + datastore.put(employees); + // [END batch_operations] + + Map got = + datastore.get(Arrays.asList(employee1.getKey(), employee2.getKey(), employee3.getKey())); + assertThat((String) got.get(employee1.getKey()).getProperty("firstName")) + .named("employee1.firstName") + .isEqualTo("Bill"); + assertThat((String) got.get(employee2.getKey()).getProperty("firstName")) + .named("employee2.firstName") + .isEqualTo("Jane"); + assertThat((String) got.get(employee3.getKey()).getProperty("firstName")) + .named("employee3.firstName") + .isEqualTo("Alex"); + } + + @Test + public void createKey_makesKey() { + // [START generating_keys_1] + Key k1 = KeyFactory.createKey("Person", "GreatGrandpa"); + Key k2 = KeyFactory.createKey("Person", 74219); + // [END generating_keys_1] + + assertThat(k1).isNotNull(); + assertThat(k2).isNotNull(); + } + + @Test + public void keyFactoryBuilder_makeKeyWithParents() { + Key greatKey = KeyFactory.createKey("Person", "GreatGrandpa"); + Key grandKey = KeyFactory.createKey(greatKey, "Person", "Grandpa"); + Key dadKey = KeyFactory.createKey(grandKey, "Person", "Dad"); + Key meKey = KeyFactory.createKey(dadKey, "Person", "Me"); + + // [START generating_keys_2] + Key k = + new KeyFactory.Builder("Person", "GreatGrandpa") + .addChild("Person", "Grandpa") + .addChild("Person", "Dad") + .addChild("Person", "Me") + .getKey(); + // [END generating_keys_2] + + assertThat(k).isEqualTo(meKey); + } + + @Test + public void keyToString_getsPerson() throws Exception { + Entity p = new Entity("Person"); + p.setProperty("relationship", "Me"); + datastore.put(p); + Key k = p.getKey(); + + // [START generating_keys_3] + String personKeyStr = KeyFactory.keyToString(k); + + // Some time later (for example, after using personKeyStr in a link). + Key personKey = KeyFactory.stringToKey(personKeyStr); + Entity person = datastore.get(personKey); + // [END generating_keys_3] + + assertThat(personKey).isEqualTo(k); + assertThat((String) person.getProperty("relationship")) + .named("person.relationship") + .isEqualTo("Me"); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/GuestbookStrongTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/GuestbookStrongTest.java new file mode 100644 index 00000000000..dcff5595dab --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/GuestbookStrongTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; + +import com.example.time.testing.FakeClock; + +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig; +import org.joda.time.Instant; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.List; + +/** + * Unit tests for {@link GuestbookStrong}. + */ +@RunWith(JUnit4.class) +public class GuestbookStrongTest { + private static final Instant FAKE_NOW = new Instant(1234567890L); + private static final String GUESTBOOK_ID = "my guestbook"; + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set maximum eventual consistency. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(100), + // Make sure there is a user logged in. We enforce this in web.xml. + new LocalUserServiceTestConfig()) + .setEnvIsLoggedIn(true) + .setEnvEmail("test@example.com") + .setEnvAuthDomain("gmail.com"); + + private FakeClock clock; + private GuestbookStrong guestbookUnderTest; + + @Before + public void setUp() throws Exception { + helper.setUp(); + clock = new FakeClock(FAKE_NOW); + guestbookUnderTest = new GuestbookStrong(GUESTBOOK_ID, clock); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void appendGreeting_normalData_setsContentProperty() { + Greeting got = guestbookUnderTest.appendGreeting("Hello, Datastore!"); + + assertThat(got.getContent()) + .named("content property") + .isEqualTo("Hello, Datastore!"); + } + + @Test + public void appendGreeting_normalData_setsDateProperty() { + Greeting got = guestbookUnderTest.appendGreeting("Hello, Datastore!"); + + assertThat(got.getDate()) + .named("date property") + .isEqualTo(FAKE_NOW); + } + + @Test + public void listGreetings_maximumEventualConsistency_returnsAllGreetings() { + // Arrange + guestbookUnderTest.appendGreeting("Hello, Datastore!"); + guestbookUnderTest.appendGreeting("Hello, Eventual Consistency!"); + guestbookUnderTest.appendGreeting("Hello, World!"); + + // Act + List got = guestbookUnderTest.listGreetings(); + + // Assert + // Since we use an ancestor query, all greetings should be available. + assertThat(got).hasSize(3); + } +} + diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/GuestbookTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/GuestbookTest.java new file mode 100644 index 00000000000..82d5fd11204 --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/GuestbookTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; + +import com.example.time.testing.FakeClock; + +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.dev.HighRepJobPolicy; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.List; + +/** + * Unit tests for {@link Guestbook}. + */ +@RunWith(JUnit4.class) +public class GuestbookTest { + private static final class CustomHighRepJobPolicy implements HighRepJobPolicy { + static int newJobCounter = 0; + static int existingJobCounter = 0; + + @Override + public boolean shouldApplyNewJob(Key entityGroup) { + // Every other new job fails to apply. + return newJobCounter++ % 2 == 0; + } + + @Override + public boolean shouldRollForwardExistingJob(Key entityGroup) { + // Existing jobs always apply after every Get and every Query. + return true; + } + } + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set custom, deterministic, eventual consistency. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setAlternateHighRepJobPolicyClass(CustomHighRepJobPolicy.class), + // Make sure there is a user logged in. We enforce this in web.xml. + new LocalUserServiceTestConfig()) + .setEnvIsLoggedIn(true) + .setEnvEmail("test@example.com") + .setEnvAuthDomain("gmail.com"); + + private FakeClock clock; + private Guestbook guestbookUnderTest; + + @Before + public void setUp() throws Exception { + helper.setUp(); + clock = new FakeClock(); + guestbookUnderTest = new Guestbook(clock); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void appendGreeting_normalData_setsContentProperty() { + Greeting got = guestbookUnderTest.appendGreeting("Hello, Datastore!"); + + assertThat(got.getContent()) + .named("content property") + .isEqualTo("Hello, Datastore!"); + } + + @Test + public void listGreetings_eventualConsistency_returnsPartialGreetings() { + // Arrange + guestbookUnderTest.appendGreeting("Hello, Datastore!"); + guestbookUnderTest.appendGreeting("Hello, Eventual Consistency!"); + guestbookUnderTest.appendGreeting("Hello, World!"); + guestbookUnderTest.appendGreeting("Güten Tag!"); + + // Act + List got = guestbookUnderTest.listGreetings(); + + // The first time we query we should half of the results due to the fact that we simulate + // eventual consistency by applying every other write. + assertThat(got).hasSize(2); + } + + @Test + public void listGreetings_groomedDatastore_returnsAllGreetings() { + // Arrange + guestbookUnderTest.appendGreeting("Hello, Datastore!"); + guestbookUnderTest.appendGreeting("Hello, Eventual Consistency!"); + guestbookUnderTest.appendGreeting("Hello, World!"); + + // Act + guestbookUnderTest.listGreetings(); + // Second global query sees both Entities because we "groom" (attempt to + // apply unapplied jobs) after every query. + List got = guestbookUnderTest.listGreetings(); + + assertThat(got).hasSize(3); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/IndexesTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/IndexesTest.java new file mode 100644 index 00000000000..2a9284f3809 --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/IndexesTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.Filter; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.datastore.Query.FilterPredicate; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.List; + +/** + * Unit tests to demonstrate App Engine Datastore queries. + */ +@RunWith(JUnit4.class) +public class IndexesTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + private DatastoreService datastore; + + @Before + public void setUp() { + helper.setUp(); + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void propertyFilterExample_returnsMatchingEntities() throws Exception { + // [START unindexed_properties_1] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + + Key acmeKey = KeyFactory.createKey("Company", "Acme"); + + Entity tom = new Entity("Person", "Tom", acmeKey); + tom.setProperty("name", "Tom"); + tom.setProperty("age", 32); + datastore.put(tom); + + Entity lucy = new Entity("Person", "Lucy", acmeKey); + lucy.setProperty("name", "Lucy"); + lucy.setUnindexedProperty("age", 29); + datastore.put(lucy); + + Filter ageFilter = new FilterPredicate("age", FilterOperator.GREATER_THAN, 25); + + Query q = new Query("Person").setAncestor(acmeKey).setFilter(ageFilter); + + // Returns tom but not lucy, because her age is unindexed + List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults()); + // [END unindexed_properties_1] + + assertThat(results).named("query results").containsExactly(tom); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/ListPeopleServletTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/ListPeopleServletTest.java new file mode 100644 index 00000000000..5ea0920a153 --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/ListPeopleServletTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.SortDirection; +import com.google.appengine.api.datastore.QueryResultList; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.common.collect.ImmutableList; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link ListPeopleServlet}. + */ +@RunWith(JUnit4.class) +public class ListPeopleServletTest { + private static final ImmutableList TEST_NAMES = + // Keep in alphabetical order, so this is the same as the query order. + ImmutableList.builder() + .add("Alpha") + .add("Bravo") + .add("Charlie") + .add("Delta") + .add("Echo") + .add("Foxtrot") + .add("Golf") + .add("Hotel") + .add("India") + .add("Juliett") + .add("Kilo") + .add("Lima") + .add("Mike") + .add("November") + .add("Oscar") + .add("Papa") + .add("Quebec") + .add("Romeo") + .add("Sierra") + .add("Tango") + .build(); + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private DatastoreService datastore; + + private ListPeopleServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + datastore = DatastoreServiceFactory.getDatastoreService(); + + // Add test data. + ImmutableList.Builder people = ImmutableList.builder(); + for (String name : TEST_NAMES) { + people.add(createPerson(name)); + } + datastore.put(people.build()); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new ListPeopleServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + private Entity createPerson(String name) { + Entity person = new Entity("Person"); + person.setProperty("name", name); + return person; + } + + @Test + public void doGet_noCursor_writesNames() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + String response = responseWriter.toString(); + for (int i = 0; i < ListPeopleServlet.PAGE_SIZE; i++) { + assertThat(response).named("ListPeopleServlet response").contains(TEST_NAMES.get(i)); + } + } + + private String getFirstCursor() { + Query q = new Query("Person").addSort("name", SortDirection.ASCENDING); + PreparedQuery pq = datastore.prepare(q); + FetchOptions fetchOptions = FetchOptions.Builder.withLimit(ListPeopleServlet.PAGE_SIZE); + QueryResultList results = pq.asQueryResultList(fetchOptions); + return results.getCursor().toWebSafeString(); + } + + @Test + public void doGet_withValidCursor_writesNames() throws Exception { + when(mockRequest.getParameter("cursor")).thenReturn(getFirstCursor()); + + servletUnderTest.doGet(mockRequest, mockResponse); + + String response = responseWriter.toString(); + int i = 0; + while (i + ListPeopleServlet.PAGE_SIZE < TEST_NAMES.size() && i < ListPeopleServlet.PAGE_SIZE) { + assertThat(response) + .named("ListPeopleServlet response") + .contains(TEST_NAMES.get(i + ListPeopleServlet.PAGE_SIZE)); + i++; + } + } + + @Test + public void doGet_withInvalidCursor_writesRedirect() throws Exception { + when(mockRequest.getParameter("cursor")).thenReturn("ThisCursorIsTotallyInvalid"); + servletUnderTest.doGet(mockRequest, mockResponse); + verify(mockResponse).sendRedirect("/people"); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataEntityGroupTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataEntityGroupTest.java new file mode 100644 index 00000000000..89545c3e208 --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataEntityGroupTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entities; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.EntityNotFoundException; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Transaction; +import com.google.appengine.api.memcache.MemcacheService; +import com.google.appengine.api.memcache.MemcacheServiceFactory; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalMemcacheServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.StringWriter; + +/** + * Unit tests to demonstrate App Engine Datastore entity group metadata. + */ +@RunWith(JUnit4.class) +public class MetadataEntityGroupTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0), + new LocalMemcacheServiceTestConfig()); + + private DatastoreService datastore; + + @Before + public void setUp() { + helper.setUp(); + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + // [START entity_group_1] + private static long getEntityGroupVersion(DatastoreService ds, Transaction tx, Key entityKey) { + try { + return Entities.getVersionProperty(ds.get(tx, Entities.createEntityGroupKey(entityKey))); + } catch (EntityNotFoundException e) { + // No entity group information, return a value strictly smaller than any + // possible version + return 0; + } + } + + private static void printEntityGroupVersions(DatastoreService ds, PrintWriter writer) { + Entity entity1 = new Entity("Simple"); + Key key1 = ds.put(entity1); + Key entityGroupKey = Entities.createEntityGroupKey(key1); + + // Print entity1's entity group version + writer.println("version " + getEntityGroupVersion(ds, null, key1)); + + // Write to a different entity group + Entity entity2 = new Entity("Simple"); + ds.put(entity2); + + // Will print the same version, as entity1's entity group has not changed + writer.println("version " + getEntityGroupVersion(ds, null, key1)); + + // Change entity1's entity group by adding a new child entity + Entity entity3 = new Entity("Simple", entity1.getKey()); + ds.put(entity3); + + // Will print a higher version, as entity1's entity group has changed + writer.println("version " + getEntityGroupVersion(ds, null, key1)); + } + // [END entity_group_1] + + @Test + public void printEntityGroupVersions_printsVersions() throws Exception { + StringWriter responseWriter = new StringWriter(); + printEntityGroupVersions(datastore, new PrintWriter(responseWriter)); + assertThat(responseWriter.toString()).contains("version"); + } + + // [START entity_group_2] + // A simple class for tracking consistent entity group counts. + private static class EntityGroupCount implements Serializable { + long version; // Version of the entity group whose count we are tracking + int count; + + EntityGroupCount(long version, int count) { + this.version = version; + this.count = count; + } + + // Display count of entities in an entity group, with consistent caching + void showEntityGroupCount( + DatastoreService ds, MemcacheService cache, PrintWriter writer, Key entityGroupKey) { + EntityGroupCount egCount = (EntityGroupCount) cache.get(entityGroupKey); + // Reuses getEntityGroupVersion method from the previous example. + if (egCount != null && egCount.version == getEntityGroupVersion(ds, null, entityGroupKey)) { + // Cached value matched current entity group version, use that + writer.println(egCount.count + " entities (cached)"); + } else { + // Need to actually count entities. Using a transaction to get a consistent count + // and entity group version. + Transaction tx = ds.beginTransaction(); + PreparedQuery pq = ds.prepare(tx, new Query(entityGroupKey)); + int count = pq.countEntities(FetchOptions.Builder.withLimit(5000)); + cache.put( + entityGroupKey, + new EntityGroupCount(getEntityGroupVersion(ds, tx, entityGroupKey), count)); + tx.rollback(); + writer.println(count + " entities"); + } + } + } + // [END entity_group_2] + + @Test + public void entityGroupCount_printsCount() throws Exception { + StringWriter responseWriter = new StringWriter(); + MemcacheService cache = MemcacheServiceFactory.getMemcacheService(); + Entity entity1 = new Entity("Simple"); + Key key1 = datastore.put(entity1); + Key entityGroupKey = Entities.createEntityGroupKey(key1); + + EntityGroupCount groupCount = new EntityGroupCount(0, 0); + groupCount.showEntityGroupCount( + datastore, cache, new PrintWriter(responseWriter), entityGroupKey); + + assertThat(responseWriter.toString()).contains(" entities"); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataKindsTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataKindsTest.java new file mode 100644 index 00000000000..16f3d7026d3 --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataKindsTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entities; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.CompositeFilterOperator; +import com.google.appengine.api.datastore.Query.Filter; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.datastore.Query.FilterPredicate; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests to demonstrate App Engine Datastore kinds metadata. + */ +@RunWith(JUnit4.class) +public class MetadataKindsTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + private StringWriter responseWriter; + private DatastoreService datastore; + + @Before + public void setUp() { + helper.setUp(); + datastore = DatastoreServiceFactory.getDatastoreService(); + responseWriter = new StringWriter(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + // [START kind_query_example] + void printLowercaseKinds(DatastoreService ds, PrintWriter writer) { + + // Start with unrestricted kind query + Query q = new Query(Entities.KIND_METADATA_KIND); + + List subFils = new ArrayList(); + + // Limit to lowercase initial letters + subFils.add( + new FilterPredicate( + Entity.KEY_RESERVED_PROPERTY, + FilterOperator.GREATER_THAN_OR_EQUAL, + Entities.createKindKey("a"))); + + String endChar = Character.toString((char) ('z' + 1)); // Character after 'z' + + subFils.add( + new FilterPredicate( + Entity.KEY_RESERVED_PROPERTY, + FilterOperator.LESS_THAN, + Entities.createKindKey(endChar))); + + q.setFilter(CompositeFilterOperator.and(subFils)); + + // Print heading + writer.println("Lowercase kinds:"); + + // Print query results + for (Entity e : ds.prepare(q).asIterable()) { + writer.println(" " + e.getKey().getName()); + } + } + // [END kind_query_example] + + @Test + public void printLowercaseKinds_printsKinds() throws Exception { + datastore.put(new Entity("alpha")); + datastore.put(new Entity("beta")); + datastore.put(new Entity("NotIncluded")); + datastore.put(new Entity("zed")); + + printLowercaseKinds(datastore, new PrintWriter(responseWriter)); + + String response = responseWriter.toString(); + assertThat(response).contains("alpha"); + assertThat(response).contains("beta"); + assertThat(response).contains("zed"); + assertThat(response).doesNotContain("NotIncluded"); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataNamespacesTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataNamespacesTest.java new file mode 100644 index 00000000000..98c09d64954 --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataNamespacesTest.java @@ -0,0 +1,153 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.appengine.api.NamespaceManager; +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entities; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.CompositeFilterOperator; +import com.google.appengine.api.datastore.Query.Filter; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.datastore.Query.FilterPredicate; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests to demonstrate App Engine Datastore namespaces metadata. + */ +@RunWith(JUnit4.class) +public class MetadataNamespacesTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + private StringWriter responseWriter; + private DatastoreService datastore; + + @Before + public void setUp() { + helper.setUp(); + datastore = DatastoreServiceFactory.getDatastoreService(); + responseWriter = new StringWriter(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + // [START queries_intro_example] + void printAllNamespaces(DatastoreService ds, PrintWriter writer) { + Query q = new Query(Entities.NAMESPACE_METADATA_KIND); + + for (Entity e : ds.prepare(q).asIterable()) { + // A nonzero numeric id denotes the default namespace; + // see Namespace Queries, below + if (e.getKey().getId() != 0) { + writer.println(""); + } else { + writer.println(e.getKey().getName()); + } + } + } + // [END queries_intro_example] + + @Test + public void printAllNamespaces_printsNamespaces() throws Exception { + datastore.put(new Entity("Simple")); + NamespaceManager.set("another-namespace"); + datastore.put(new Entity("Simple")); + + printAllNamespaces(datastore, new PrintWriter(responseWriter)); + + String response = responseWriter.toString(); + assertThat(response).contains(""); + assertThat(response).contains("another-namespace"); + } + + // [START namespace_query_example] + List getNamespaces(DatastoreService ds, String start, String end) { + + // Start with unrestricted namespace query + Query q = new Query(Entities.NAMESPACE_METADATA_KIND); + List subFilters = new ArrayList(); + // Limit to specified range, if any + if (start != null) { + subFilters.add( + new FilterPredicate( + Entity.KEY_RESERVED_PROPERTY, + FilterOperator.GREATER_THAN_OR_EQUAL, + Entities.createNamespaceKey(start))); + } + if (end != null) { + subFilters.add( + new FilterPredicate( + Entity.KEY_RESERVED_PROPERTY, + FilterOperator.LESS_THAN_OR_EQUAL, + Entities.createNamespaceKey(end))); + } + + q.setFilter(CompositeFilterOperator.and(subFilters)); + + // Initialize result list + List results = new ArrayList(); + + // Build list of query results + for (Entity e : ds.prepare(q).asIterable()) { + results.add(Entities.getNamespaceFromNamespaceKey(e.getKey())); + } + + // Return result list + return results; + } + // [END namespace_query_example] + + @Test + public void getNamespaces_returnsNamespaces() throws Exception { + NamespaceManager.set("alpha"); + datastore.put(new Entity("Simple")); + NamespaceManager.set("bravo"); + datastore.put(new Entity("Simple")); + NamespaceManager.set("charlie"); + datastore.put(new Entity("Simple")); + NamespaceManager.set("zed"); + datastore.put(new Entity("Simple")); + + List results = getNamespaces(datastore, "bravo", "echo"); + + assertThat(results).containsExactly("bravo", "charlie"); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataPropertiesTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataPropertiesTest.java new file mode 100644 index 00000000000..64b32c6dc38 --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/MetadataPropertiesTest.java @@ -0,0 +1,234 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entities; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.CompositeFilterOperator; +import com.google.appengine.api.datastore.Query.FilterPredicate; +import com.google.appengine.api.datastore.Query.SortDirection; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +/** + * Unit tests to demonstrate App Engine Datastore properties metadata. + */ +@RunWith(JUnit4.class) +public class MetadataPropertiesTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + private StringWriter responseWriter; + private DatastoreService datastore; + + @Before + public void setUp() { + helper.setUp(); + datastore = DatastoreServiceFactory.getDatastoreService(); + responseWriter = new StringWriter(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + // [START property_query_example] + void printProperties(DatastoreService ds, PrintWriter writer) { + + // Create unrestricted keys-only property query + Query q = new Query(Entities.PROPERTY_METADATA_KIND).setKeysOnly(); + + // Print query results + for (Entity e : ds.prepare(q).asIterable()) { + writer.println(e.getKey().getParent().getName() + ": " + e.getKey().getName()); + } + } + // [END property_query_example] + + @Test + public void printProperties_printsProperties() throws Exception { + Entity a = new Entity("Widget"); + a.setProperty("combobulators", 2); + a.setProperty("oscillatorState", "harmonzing"); + Entity b = new Entity("Ship"); + b.setProperty("sails", 2); + b.setProperty("captain", "Blackbeard"); + Entity c = new Entity("Ship"); + c.setProperty("captain", "Redbeard"); + c.setProperty("motor", "outboard"); + datastore.put(Arrays.asList(a, b, c)); + + printProperties(datastore, new PrintWriter(responseWriter)); + + String response = responseWriter.toString(); + assertThat(response).contains("Widget: combobulators"); + assertThat(response).contains("Widget: oscillatorState"); + assertThat(response).contains("Ship: sails"); + assertThat(response).contains("Ship: captain"); + assertThat(response).contains("Ship: motor"); + } + + // [START property_filtering_example] + void printPropertyRange(DatastoreService ds, PrintWriter writer) { + + // Start with unrestricted keys-only property query + Query q = new Query(Entities.PROPERTY_METADATA_KIND).setKeysOnly(); + + // Limit range + q.setFilter( + CompositeFilterOperator.and( + new FilterPredicate( + Entity.KEY_RESERVED_PROPERTY, + Query.FilterOperator.GREATER_THAN_OR_EQUAL, + Entities.createPropertyKey("Employee", "salary")), + new FilterPredicate( + Entity.KEY_RESERVED_PROPERTY, + Query.FilterOperator.LESS_THAN_OR_EQUAL, + Entities.createPropertyKey("Manager", "salary")))); + q.addSort(Entity.KEY_RESERVED_PROPERTY, SortDirection.ASCENDING); + + // Print query results + for (Entity e : ds.prepare(q).asIterable()) { + writer.println(e.getKey().getParent().getName() + ": " + e.getKey().getName()); + } + } + // [END property_filtering_example] + + @Test + public void printPropertyRange_printsProperties() throws Exception { + Entity account = new Entity("Account"); + account.setProperty("balance", "10.30"); + account.setProperty("company", "General Company"); + Entity employee = new Entity("Employee"); + employee.setProperty("name", "John Doe"); + employee.setProperty("ssn", "987-65-4321"); + Entity invoice = new Entity("Invoice"); + invoice.setProperty("date", new Date()); + invoice.setProperty("amount", "99.98"); + Entity manager = new Entity("Manager"); + manager.setProperty("name", "Jane Doe"); + manager.setProperty("title", "Technical Director"); + Entity product = new Entity("Product"); + product.setProperty("description", "Widget to re-ionize an oscillator"); + product.setProperty("price", "19.97"); + datastore.put(Arrays.asList(account, employee, invoice, manager, product)); + + printPropertyRange(datastore, new PrintWriter(responseWriter)); + + String response = responseWriter.toString(); + assertThat(response) + .isEqualTo("Employee: ssn\nInvoice: amount\nInvoice: date\nManager: name\n"); + } + + // [START property_ancestor_query_example] + List propertiesOfKind(DatastoreService ds, String kind) { + + // Start with unrestricted keys-only property query + Query q = new Query(Entities.PROPERTY_METADATA_KIND).setKeysOnly(); + + // Limit to specified kind + q.setAncestor(Entities.createKindKey(kind)); + + // Initialize result list + ArrayList results = new ArrayList(); + + //Build list of query results + for (Entity e : ds.prepare(q).asIterable()) { + results.add(e.getKey().getName()); + } + + // Return result list + return results; + } + // [END property_ancestor_query_example] + + @Test + public void propertiesOfKind_returnsProperties() throws Exception { + Entity a = new Entity("Alpha"); + a.setProperty("beta", 12); + a.setProperty("charlie", "misc."); + Entity b = new Entity("Alpha"); + b.setProperty("charlie", "assorted"); + b.setProperty("delta", new Date()); + Entity c = new Entity("Charlie"); + c.setProperty("charlie", "some"); + c.setProperty("echo", new Date()); + datastore.put(Arrays.asList(a, b, c)); + + List properties = propertiesOfKind(datastore, "Alpha"); + + assertThat(properties).containsExactly("beta", "charlie", "delta"); + } + + // [START property_representation_query_example] + Collection representationsOfProperty(DatastoreService ds, String kind, String property) { + + // Start with unrestricted non-keys-only property query + Query q = new Query(Entities.PROPERTY_METADATA_KIND); + + // Limit to specified kind and property + q.setFilter( + new FilterPredicate( + "__key__", Query.FilterOperator.EQUAL, Entities.createPropertyKey(kind, property))); + + // Get query result + Entity propInfo = ds.prepare(q).asSingleEntity(); + + // Return collection of property representations + return (Collection) propInfo.getProperty("property_representation"); + } + // [END property_representation_query_example] + + @Test + public void representationsOfProperty_returnsRepresentations() throws Exception { + Entity a = new Entity("Alpha"); + a.setProperty("beta", 12); + Entity b = new Entity("Alpha"); + b.setProperty("beta", true); + Entity c = new Entity("Alpha"); + c.setProperty("beta", new Date()); + datastore.put(Arrays.asList(a, b, c)); + + Collection results = representationsOfProperty(datastore, "Alpha", "beta"); + + assertThat(results).containsExactly("INT64", "BOOLEAN"); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/ProjectionServletTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/ProjectionServletTest.java new file mode 100644 index 00000000000..101abf06052 --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/ProjectionServletTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.example.time.testing.FakeClock; + +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link ProjectionServlet}. + */ +@RunWith(JUnit4.class) +public class ProjectionServletTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig()); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private ProjectionServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new ProjectionServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_emptyDatastore_writesNoGreetings() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + assertThat(responseWriter.toString()) + .named("ProjectionServlet response") + .doesNotContain("Message"); + } + + @Test + public void doGet_manyGreetings_writesLatestGreetings() throws Exception { + // Arrange + GuestbookStrong guestbook = + new GuestbookStrong(GuestbookStrongServlet.GUESTBOOK_ID, new FakeClock()); + guestbook.appendGreeting("Hello."); + guestbook.appendGreeting("Güten Tag!"); + guestbook.appendGreeting("Hi."); + guestbook.appendGreeting("Hola."); + + // Act + servletUnderTest.doGet(mockRequest, mockResponse); + String output = responseWriter.toString(); + + assertThat(output) + .named("ProjectionServlet response") + .contains("Message Hello."); + assertThat(output) + .named("ProjectionServlet response") + .contains("Message Güten Tag!"); + assertThat(output) + .named("ProjectionServlet response") + .contains("Message Hola."); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/ProjectionTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/ProjectionTest.java new file mode 100644 index 00000000000..e8f3e81533b --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/ProjectionTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.PropertyProjection; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.List; + +/** + * Unit tests to demonstrate App Engine Datastore projection queries. + */ +@RunWith(JUnit4.class) +public class ProjectionTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + private DatastoreService datastore; + + @Before + public void setUp() throws Exception { + helper.setUp(); + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + + @Test + public void projectionQuery_grouping_filtersDuplicates() { + putTestData("some duplicate", 0L); + putTestData("some duplicate", 0L); + putTestData("too big", 1L); + + // [START grouping] + Query q = new Query("TestKind"); + q.addProjection(new PropertyProjection("A", String.class)); + q.addProjection(new PropertyProjection("B", Long.class)); + q.setDistinct(true); + q.setFilter(Query.FilterOperator.LESS_THAN.of("B", 1L)); + q.addSort("B", Query.SortDirection.DESCENDING); + q.addSort("A"); + // [END grouping] + + List entities = + datastore.prepare(q).asList(FetchOptions.Builder.withLimit(5)); + assertThat(entities).hasSize(1); + Entity entity = entities.get(0); + assertThat((String) entity.getProperty("A")).named("entity.A").isEqualTo("some duplicate"); + assertThat((long) entity.getProperty("B")).named("entity.B").isEqualTo(0L); + } + + private void putTestData(String a, long b) { + Entity entity = new Entity("TestKind"); + entity.setProperty("A", a); + entity.setProperty("B", b); + datastore.put(entity); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/QueriesTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/QueriesTest.java new file mode 100644 index 00000000000..d8a2e61dd5c --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/QueriesTest.java @@ -0,0 +1,856 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.PreparedQuery.TooManyResultsException; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.CompositeFilter; +import com.google.appengine.api.datastore.Query.CompositeFilterOperator; +import com.google.appengine.api.datastore.Query.Filter; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.datastore.Query.FilterPredicate; +import com.google.appengine.api.datastore.Query.SortDirection; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.common.collect.ImmutableList; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; + +/** + * Unit tests to demonstrate App Engine Datastore queries. + */ +@RunWith(JUnit4.class) +public class QueriesTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + private DatastoreService datastore; + + @Before + public void setUp() { + helper.setUp(); + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void propertyFilterExample_returnsMatchingEntities() throws Exception { + // Arrange + Entity p1 = new Entity("Person"); + p1.setProperty("height", 120); + Entity p2 = new Entity("Person"); + p2.setProperty("height", 180); + Entity p3 = new Entity("Person"); + p3.setProperty("height", 160); + datastore.put(ImmutableList.of(p1, p2, p3)); + + // Act + long minHeight = 160; + // [START property_filter_example] + Filter propertyFilter = + new FilterPredicate("height", FilterOperator.GREATER_THAN_OR_EQUAL, minHeight); + Query q = new Query("Person").setFilter(propertyFilter); + // [END property_filter_example] + + // Assert + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(p2, p3); + } + + @Test + public void keyFilterExample_returnsMatchingEntities() throws Exception { + // Arrange + Entity a = new Entity("Person", "a"); + Entity b = new Entity("Person", "b"); + Entity c = new Entity("Person", "c"); + Entity aa = new Entity("Person", "aa", b.getKey()); + Entity bb = new Entity("Person", "bb", b.getKey()); + Entity aaa = new Entity("Person", "aaa", bb.getKey()); + Entity bbb = new Entity("Person", "bbb", bb.getKey()); + datastore.put(ImmutableList.of(a, b, c, aa, bb, aaa, bbb)); + + // Act + Key lastSeenKey = bb.getKey(); + // [START key_filter_example] + Filter keyFilter = + new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, lastSeenKey); + Query q = new Query("Person").setFilter(keyFilter); + // [END key_filter_example] + + // Assert + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results) + .named("query results") + .containsExactly( + aaa, // Ancestor path "b/bb/aaa" is greater than "b/bb". + bbb, // Ancestor path "b/bb/bbb" is greater than "b/bb". + c); // Key name identifier "c" is greater than b. + } + + @Test + public void keyFilterExample_kindless_returnsMatchingEntities() throws Exception { + // Arrange + Entity a = new Entity("Child", "a"); + Entity b = new Entity("Child", "b"); + Entity c = new Entity("Child", "c"); + Entity aa = new Entity("Child", "aa", b.getKey()); + Entity bb = new Entity("Child", "bb", b.getKey()); + Entity aaa = new Entity("Child", "aaa", bb.getKey()); + Entity bbb = new Entity("Child", "bbb", bb.getKey()); + Entity adult = new Entity("Adult", "a"); + Entity zooAnimal = new Entity("ZooAnimal", "a"); + datastore.put(ImmutableList.of(a, b, c, aa, bb, aaa, bbb, adult, zooAnimal)); + + // Act + Key lastSeenKey = bb.getKey(); + // [START kindless_query_example] + Filter keyFilter = + new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, lastSeenKey); + Query q = new Query().setFilter(keyFilter); + // [END kindless_query_example] + + // Assert + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results) + .named("query results") + .containsExactly( + aaa, // Ancestor path "b/bb/aaa" is greater than "b/bb". + bbb, // Ancestor path "b/bb/bbb" is greater than "b/bb". + zooAnimal, // Kind "ZooAnimal" is greater than "Child" + c); // Key name identifier "c" is greater than b. + } + + @Test + public void ancestorFilterExample_returnsMatchingEntities() throws Exception { + Entity a = new Entity("Person", "a"); + Entity b = new Entity("Person", "b"); + Entity aa = new Entity("Person", "aa", a.getKey()); + Entity ab = new Entity("Person", "ab", a.getKey()); + Entity bb = new Entity("Person", "bb", b.getKey()); + datastore.put(ImmutableList.of(a, b, aa, ab, bb)); + + Key ancestorKey = a.getKey(); + // [START ancestor_filter_example] + Query q = new Query("Person").setAncestor(ancestorKey); + // [END ancestor_filter_example] + + // Assert + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(a, aa, ab); + } + + @Test + public void ancestorQueryExample_returnsMatchingEntities() throws Exception { + // [START ancestor_query_example] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + + Entity tom = new Entity("Person", "Tom"); + Key tomKey = tom.getKey(); + datastore.put(tom); + + Entity weddingPhoto = new Entity("Photo", tomKey); + weddingPhoto.setProperty("imageURL", "http://domain.com/some/path/to/wedding_photo.jpg"); + + Entity babyPhoto = new Entity("Photo", tomKey); + babyPhoto.setProperty("imageURL", "http://domain.com/some/path/to/baby_photo.jpg"); + + Entity dancePhoto = new Entity("Photo", tomKey); + dancePhoto.setProperty("imageURL", "http://domain.com/some/path/to/dance_photo.jpg"); + + Entity campingPhoto = new Entity("Photo"); + campingPhoto.setProperty("imageURL", "http://domain.com/some/path/to/camping_photo.jpg"); + + List photoList = Arrays.asList(weddingPhoto, babyPhoto, dancePhoto, campingPhoto); + datastore.put(photoList); + + Query photoQuery = new Query("Photo").setAncestor(tomKey); + + // This returns weddingPhoto, babyPhoto, and dancePhoto, + // but not campingPhoto, because tom is not an ancestor + List results = + datastore.prepare(photoQuery).asList(FetchOptions.Builder.withDefaults()); + // [END ancestor_query_example] + + assertThat(results).named("query results").containsExactly(weddingPhoto, babyPhoto, dancePhoto); + } + + @Test + public void ancestorQueryExample_kindlessKeyFilter_returnsMatchingEntities() throws Exception { + // Arrange + Entity a = new Entity("Grandparent", "a"); + Entity b = new Entity("Grandparent", "b"); + Entity c = new Entity("Grandparent", "c"); + Entity aa = new Entity("Parent", "aa", a.getKey()); + Entity ba = new Entity("Parent", "ba", b.getKey()); + Entity bb = new Entity("Parent", "bb", b.getKey()); + Entity bc = new Entity("Parent", "bc", b.getKey()); + Entity cc = new Entity("Parent", "cc", c.getKey()); + Entity aaa = new Entity("Child", "aaa", aa.getKey()); + Entity bbb = new Entity("Child", "bbb", bb.getKey()); + datastore.put(ImmutableList.of(a, b, c, aa, ba, bb, bc, cc, aaa, bbb)); + + // Act + Key ancestorKey = b.getKey(); + Key lastSeenKey = bb.getKey(); + // [START kindless_ancestor_key_query_example] + Filter keyFilter = + new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, lastSeenKey); + Query q = new Query().setAncestor(ancestorKey).setFilter(keyFilter); + // [END kindless_ancestor_key_query_example] + + // Assert + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(bc, bbb); + } + + @Test + public void ancestorQueryExample_kindlessKeyFilterFull_returnsMatchingEntities() + throws Exception { + // [START kindless_ancestor_query_example] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + + Entity tom = new Entity("Person", "Tom"); + Key tomKey = tom.getKey(); + datastore.put(tom); + + Entity weddingPhoto = new Entity("Photo", tomKey); + weddingPhoto.setProperty("imageURL", "http://domain.com/some/path/to/wedding_photo.jpg"); + + Entity weddingVideo = new Entity("Video", tomKey); + weddingVideo.setProperty("videoURL", "http://domain.com/some/path/to/wedding_video.avi"); + + List mediaList = Arrays.asList(weddingPhoto, weddingVideo); + datastore.put(mediaList); + + // By default, ancestor queries include the specified ancestor itself. + // The following filter excludes the ancestor from the query results. + Filter keyFilter = + new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, tomKey); + + Query mediaQuery = new Query().setAncestor(tomKey).setFilter(keyFilter); + + // Returns both weddingPhoto and weddingVideo, + // even though they are of different entity kinds + List results = + datastore.prepare(mediaQuery).asList(FetchOptions.Builder.withDefaults()); + // [END kindless_ancestor_query_example] + + assertThat(results).named("query result keys").containsExactly(weddingPhoto, weddingVideo); + } + + @Test + public void keysOnlyExample_returnsMatchingEntities() throws Exception { + // Arrange + Entity a = new Entity("Person", "a"); + Entity b = new Entity("Building", "b"); + Entity c = new Entity("Person", "c"); + datastore.put(ImmutableList.of(a, b, c)); + + // [START keys_only_example] + Query q = new Query("Person").setKeysOnly(); + // [END keys_only_example] + + // Assert + List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(a, c); + } + + @Test + public void sortOrderExample_returnsSortedEntities() throws Exception { + // Arrange + Entity a = new Entity("Person", "a"); + a.setProperty("lastName", "Alpha"); + a.setProperty("height", 100); + Entity b = new Entity("Person", "b"); + b.setProperty("lastName", "Bravo"); + b.setProperty("height", 200); + Entity c = new Entity("Person", "c"); + c.setProperty("lastName", "Charlie"); + c.setProperty("height", 300); + datastore.put(ImmutableList.of(a, b, c)); + + // Act + // [START sort_order_example] + // Order alphabetically by last name: + Query q1 = new Query("Person").addSort("lastName", SortDirection.ASCENDING); + + // Order by height, tallest to shortest: + Query q2 = new Query("Person").addSort("height", SortDirection.DESCENDING); + // [END sort_order_example] + + // Assert + List lastNameResults = + datastore.prepare(q1.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(lastNameResults).named("last name query results").containsExactly(a, b, c).inOrder(); + List heightResults = + datastore.prepare(q2.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(heightResults).named("height query results").containsExactly(c, b, a).inOrder(); + } + + @Test + public void sortOrderExample_multipleSortOrders_returnsSortedEntities() throws Exception { + // Arrange + Entity a = new Entity("Person", "a"); + a.setProperty("lastName", "Alpha"); + a.setProperty("height", 100); + Entity b1 = new Entity("Person", "b1"); + b1.setProperty("lastName", "Bravo"); + b1.setProperty("height", 150); + Entity b2 = new Entity("Person", "b2"); + b2.setProperty("lastName", "Bravo"); + b2.setProperty("height", 200); + Entity c = new Entity("Person", "c"); + c.setProperty("lastName", "Charlie"); + c.setProperty("height", 300); + datastore.put(ImmutableList.of(a, b1, b2, c)); + + // Act + // [START multiple_sort_orders_example] + Query q = + new Query("Person") + .addSort("lastName", SortDirection.ASCENDING) + .addSort("height", SortDirection.DESCENDING); + // [END multiple_sort_orders_example] + + // Assert + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(a, b2, b1, c).inOrder(); + } + + @Test + public void queryInterface_multipleFilters_printsMatchedEntities() throws Exception { + // Arrange + Entity a = new Entity("Person", "a"); + a.setProperty("firstName", "Alph"); + a.setProperty("lastName", "Alpha"); + a.setProperty("height", 60); + Entity b = new Entity("Person", "b"); + b.setProperty("firstName", "Bee"); + b.setProperty("lastName", "Bravo"); + b.setProperty("height", 70); + Entity c = new Entity("Person", "c"); + c.setProperty("firstName", "Charles"); + c.setProperty("lastName", "Charlie"); + c.setProperty("height", 100); + datastore.put(ImmutableList.of(a, b, c)); + + StringWriter buf = new StringWriter(); + PrintWriter out = new PrintWriter(buf); + long minHeight = 60; + long maxHeight = 72; + + // Act + // [START interface_1] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + + Filter heightMinFilter = + new FilterPredicate("height", FilterOperator.GREATER_THAN_OR_EQUAL, minHeight); + + Filter heightMaxFilter = + new FilterPredicate("height", FilterOperator.LESS_THAN_OR_EQUAL, maxHeight); + + // Use CompositeFilter to combine multiple filters + CompositeFilter heightRangeFilter = + CompositeFilterOperator.and(heightMinFilter, heightMaxFilter); + + // Use class Query to assemble a query + Query q = new Query("Person").setFilter(heightRangeFilter); + + // Use PreparedQuery interface to retrieve results + PreparedQuery pq = datastore.prepare(q); + + for (Entity result : pq.asIterable()) { + String firstName = (String) result.getProperty("firstName"); + String lastName = (String) result.getProperty("lastName"); + Long height = (Long) result.getProperty("height"); + + out.println(firstName + " " + lastName + ", " + height + " inches tall"); + } + // [END interface_1] + + // Assert + assertThat(buf.toString()).contains("Alph Alpha, 60 inches tall"); + assertThat(buf.toString()).contains("Bee Bravo, 70 inches tall"); + assertThat(buf.toString()).doesNotContain("Charlie"); + } + + @Test + public void queryInterface_singleFilter_returnsMatchedEntities() throws Exception { + // Arrange + Entity a = new Entity("Person", "a"); + a.setProperty("height", 100); + Entity b = new Entity("Person", "b"); + b.setProperty("height", 150); + Entity c = new Entity("Person", "c"); + c.setProperty("height", 300); + datastore.put(ImmutableList.of(a, b, c)); + + // Act + long minHeight = 150; + // [START interface_2] + Filter heightMinFilter = + new FilterPredicate("height", FilterOperator.GREATER_THAN_OR_EQUAL, minHeight); + + Query q = new Query("Person").setFilter(heightMinFilter); + // [END interface_2] + + // Assert + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(b, c); + } + + @Test + public void queryInterface_orFilter_printsMatchedEntities() throws Exception { + // Arrange + Entity a = new Entity("Person", "a"); + a.setProperty("height", 100); + Entity b = new Entity("Person", "b"); + b.setProperty("height", 150); + Entity c = new Entity("Person", "c"); + c.setProperty("height", 200); + datastore.put(ImmutableList.of(a, b, c)); + + StringWriter buf = new StringWriter(); + PrintWriter out = new PrintWriter(buf); + long minHeight = 125; + long maxHeight = 175; + + // Act + // [START interface_3] + Filter tooShortFilter = new FilterPredicate("height", FilterOperator.LESS_THAN, minHeight); + + Filter tooTallFilter = new FilterPredicate("height", FilterOperator.GREATER_THAN, maxHeight); + + Filter heightOutOfRangeFilter = CompositeFilterOperator.or(tooShortFilter, tooTallFilter); + + Query q = new Query("Person").setFilter(heightOutOfRangeFilter); + // [END interface_3] + + // Assert + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(a, c); + } + + @Test + public void queryRestrictions_compositeFilter_returnsMatchedEntities() throws Exception { + // Arrange + Entity a = new Entity("Person", "a"); + a.setProperty("birthYear", 1930); + Entity b = new Entity("Person", "b"); + b.setProperty("birthYear", 1960); + Entity c = new Entity("Person", "c"); + c.setProperty("birthYear", 1990); + datastore.put(ImmutableList.of(a, b, c)); + + // Act + long minBirthYear = 1940; + long maxBirthYear = 1980; + // [START inequality_filters_one_property_valid_example_1] + Filter birthYearMinFilter = + new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear); + + Filter birthYearMaxFilter = + new FilterPredicate("birthYear", FilterOperator.LESS_THAN_OR_EQUAL, maxBirthYear); + + Filter birthYearRangeFilter = + CompositeFilterOperator.and(birthYearMinFilter, birthYearMaxFilter); + + Query q = new Query("Person").setFilter(birthYearRangeFilter); + // [END inequality_filters_one_property_valid_example_1] + + // Assert + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(b); + } + + @Test + public void queryRestrictions_compositeFilter_isInvalid() throws Exception { + long minBirthYear = 1940; + long maxHeight = 200; + // [START inequality_filters_one_property_invalid_example] + Filter birthYearMinFilter = + new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear); + + Filter heightMaxFilter = + new FilterPredicate("height", FilterOperator.LESS_THAN_OR_EQUAL, maxHeight); + + Filter invalidFilter = CompositeFilterOperator.and(birthYearMinFilter, heightMaxFilter); + + Query q = new Query("Person").setFilter(invalidFilter); + // [END inequality_filters_one_property_invalid_example] + + // Note: The local devserver behavior is different than the production + // version of Cloud Datastore, so there aren't any assertions we can make + // in this test. The query appears to work with the local test runner, + // but will fail in production. + } + + @Test + public void queryRestrictions_compositeEqualFilter_returnsMatchedEntities() throws Exception { + // Arrange + Entity a = new Entity("Person", "a"); + a.setProperty("birthYear", 1930); + a.setProperty("city", "Somewhere"); + a.setProperty("lastName", "Someone"); + Entity b = new Entity("Person", "b"); + b.setProperty("birthYear", 1960); + b.setProperty("city", "Somewhere"); + b.setProperty("lastName", "Someone"); + Entity c = new Entity("Person", "c"); + c.setProperty("birthYear", 1990); + c.setProperty("city", "Somewhere"); + c.setProperty("lastName", "Someone"); + Entity d = new Entity("Person", "d"); + d.setProperty("birthYear", 1960); + d.setProperty("city", "Nowhere"); + d.setProperty("lastName", "Someone"); + Entity e = new Entity("Person", "e"); + e.setProperty("birthYear", 1960); + e.setProperty("city", "Somewhere"); + e.setProperty("lastName", "Noone"); + datastore.put(ImmutableList.of(a, b, c, d, e)); + long minBirthYear = 1940; + long maxBirthYear = 1980; + String targetCity = "Somewhere"; + String targetLastName = "Someone"; + + // [START inequality_filters_one_property_valid_example_2] + Filter lastNameFilter = new FilterPredicate("lastName", FilterOperator.EQUAL, targetLastName); + + Filter cityFilter = new FilterPredicate("city", FilterOperator.EQUAL, targetCity); + + Filter birthYearMinFilter = + new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear); + + Filter birthYearMaxFilter = + new FilterPredicate("birthYear", FilterOperator.LESS_THAN_OR_EQUAL, maxBirthYear); + + Filter validFilter = + CompositeFilterOperator.and( + lastNameFilter, cityFilter, birthYearMinFilter, birthYearMaxFilter); + + Query q = new Query("Person").setFilter(validFilter); + // [END inequality_filters_one_property_valid_example_2] + + // Assert + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(b); + } + + @Test + public void queryRestrictions_inequalitySortedFirst_returnsMatchedEntities() throws Exception { + // Arrange + Entity a = new Entity("Person", "a"); + a.setProperty("birthYear", 1930); + a.setProperty("lastName", "Someone"); + Entity b = new Entity("Person", "b"); + b.setProperty("birthYear", 1990); + b.setProperty("lastName", "Bravo"); + Entity c = new Entity("Person", "c"); + c.setProperty("birthYear", 1960); + c.setProperty("lastName", "Charlie"); + Entity d = new Entity("Person", "d"); + d.setProperty("birthYear", 1960); + d.setProperty("lastName", "Delta"); + datastore.put(ImmutableList.of(a, b, c, d)); + long minBirthYear = 1940; + + // [START inequality_filters_sort_orders_valid_example] + Filter birthYearMinFilter = + new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear); + + Query q = + new Query("Person") + .setFilter(birthYearMinFilter) + .addSort("birthYear", SortDirection.ASCENDING) + .addSort("lastName", SortDirection.ASCENDING); + // [END inequality_filters_sort_orders_valid_example] + + // Assert + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(c, d, b).inOrder(); + } + + @Test + public void queryRestrictions_missingSortOnInequality_isInvalid() throws Exception { + long minBirthYear = 1940; + // [START inequality_filters_sort_orders_invalid_example_1] + Filter birthYearMinFilter = + new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear); + + // Not valid. Missing sort on birthYear. + Query q = + new Query("Person") + .setFilter(birthYearMinFilter) + .addSort("lastName", SortDirection.ASCENDING); + // [END inequality_filters_sort_orders_invalid_example_1] + + // Note: The local devserver behavior is different than the production + // version of Cloud Datastore, so there aren't any assertions we can make + // in this test. The query appears to work with the local test runner, + // but will fail in production. + } + + @Test + public void queryRestrictions_sortWrongOrderOnInequality_isInvalid() throws Exception { + long minBirthYear = 1940; + // [START inequality_filters_sort_orders_invalid_example_2] + Filter birthYearMinFilter = + new FilterPredicate("birthYear", FilterOperator.GREATER_THAN_OR_EQUAL, minBirthYear); + + // Not valid. Sort on birthYear needs to be first. + Query q = + new Query("Person") + .setFilter(birthYearMinFilter) + .addSort("lastName", SortDirection.ASCENDING) + .addSort("birthYear", SortDirection.ASCENDING); + // [END inequality_filters_sort_orders_invalid_example_2] + + // Note: The local devserver behavior is different than the production + // version of Cloud Datastore, so there aren't any assertions we can make + // in this test. The query appears to work with the local test runner, + // but will fail in production. + } + + @Test + public void queryRestrictions_surprisingMultipleValuesAllMustMatch_returnsNoEntities() + throws Exception { + Entity a = new Entity("Widget", "a"); + List xs = Arrays.asList(1L, 2L); + a.setProperty("x", xs); + datastore.put(a); + + // [START surprising_behavior_example_1] + Query q = + new Query("Widget") + .setFilter( + CompositeFilterOperator.and( + new FilterPredicate("x", FilterOperator.GREATER_THAN, 1), + new FilterPredicate("x", FilterOperator.LESS_THAN, 2))); + // [END surprising_behavior_example_1] + + // Entity "a" will not match because no individual value matches all filters. + // See the documentation for more details: + // https://cloud.google.com/appengine/docs/java/datastore/query-restrictions#properties_with_multiple_values_can_behave_in_surprising_ways + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").isEmpty(); + } + + @Test + public void queryRestrictions_surprisingMultipleValuesEquals_returnsMatchedEntities() + throws Exception { + Entity a = new Entity("Widget", "a"); + a.setProperty("x", ImmutableList.of(1L, 2L)); + Entity b = new Entity("Widget", "b"); + b.setProperty("x", ImmutableList.of(1L, 3L)); + Entity c = new Entity("Widget", "c"); + c.setProperty("x", ImmutableList.of(-6L, 2L)); + Entity d = new Entity("Widget", "d"); + d.setProperty("x", ImmutableList.of(-6L, 4L)); + Entity e = new Entity("Widget", "e"); + e.setProperty("x", ImmutableList.of(1L, 2L, 3L)); + datastore.put(ImmutableList.of(a, b, c, d, e)); + + // [START surprising_behavior_example_2] + Query q = + new Query("Widget") + .setFilter( + CompositeFilterOperator.and( + new FilterPredicate("x", FilterOperator.EQUAL, 1), + new FilterPredicate("x", FilterOperator.EQUAL, 2))); + // [END surprising_behavior_example_2] + + // Only "a" and "e" have both 1 and 2 in the "x" array-valued property. + // See the documentation for more details: + // https://cloud.google.com/appengine/docs/java/datastore/query-restrictions#properties_with_multiple_values_can_behave_in_surprising_ways + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(a, e); + } + + @Test + public void queryRestrictions_surprisingMultipleValuesNotEquals_returnsMatchedEntities() + throws Exception { + Entity a = new Entity("Widget", "a"); + a.setProperty("x", ImmutableList.of(1L, 2L)); + Entity b = new Entity("Widget", "b"); + b.setProperty("x", ImmutableList.of(1L, 3L)); + Entity c = new Entity("Widget", "c"); + c.setProperty("x", ImmutableList.of(-6L, 2L)); + Entity d = new Entity("Widget", "d"); + d.setProperty("x", ImmutableList.of(-6L, 4L)); + Entity e = new Entity("Widget", "e"); + e.setProperty("x", ImmutableList.of(1L)); + datastore.put(ImmutableList.of(a, b, c, d, e)); + + // [START surprising_behavior_example_3] + Query q = new Query("Widget").setFilter(new FilterPredicate("x", FilterOperator.NOT_EQUAL, 1)); + // [END surprising_behavior_example_3] + + // The query matches any entity that has a some value other than 1. Only + // entity "e" is not matched. See the documentation for more details: + // https://cloud.google.com/appengine/docs/java/datastore/query-restrictions#properties_with_multiple_values_can_behave_in_surprising_ways + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(a, b, c, d); + } + + @Test + public void queryRestrictions_surprisingMultipleValuesTwoNotEquals_returnsMatchedEntities() + throws Exception { + Entity a = new Entity("Widget", "a"); + a.setProperty("x", ImmutableList.of(1L, 2L)); + Entity b = new Entity("Widget", "b"); + b.setProperty("x", ImmutableList.of(1L, 2L, 3L)); + datastore.put(ImmutableList.of(a, b)); + + // [START surprising_behavior_example_4] + Query q = + new Query("Widget") + .setFilter( + CompositeFilterOperator.and( + new FilterPredicate("x", FilterOperator.NOT_EQUAL, 1), + new FilterPredicate("x", FilterOperator.NOT_EQUAL, 2))); + // [END surprising_behavior_example_4] + + // The two NOT_EQUAL filters in the query become like the combination of queries: + // x < 1 OR (x > 1 AND x < 2) OR x > 2 + // + // Only "b" has some value which matches the "x > 2" portion of this query. + // + // See the documentation for more details: + // https://cloud.google.com/appengine/docs/java/datastore/query-restrictions#properties_with_multiple_values_can_behave_in_surprising_ways + List results = + datastore.prepare(q.setKeysOnly()).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").containsExactly(b); + } + + private Entity retrievePersonWithLastName(String targetLastName) { + // [START single_retrieval_example] + Query q = + new Query("Person") + .setFilter(new FilterPredicate("lastName", FilterOperator.EQUAL, targetLastName)); + + PreparedQuery pq = datastore.prepare(q); + Entity result = pq.asSingleEntity(); + // [END single_retrieval_example] + return result; + } + + @Test + public void singleRetrievalExample_singleEntity_returnsEntity() throws Exception { + Entity a = new Entity("Person", "a"); + a.setProperty("lastName", "Johnson"); + Entity b = new Entity("Person", "b"); + b.setProperty("lastName", "Smith"); + datastore.put(ImmutableList.of(a, b)); + + Entity result = retrievePersonWithLastName("Johnson"); + + assertThat(result).named("result").isEqualTo(a); // Note: Entity.equals() only checks the Key. + } + + @Test + public void singleRetrievalExample_multitpleEntities_throwsException() throws Exception { + Entity a = new Entity("Person", "a"); + a.setProperty("lastName", "Johnson"); + Entity b = new Entity("Person", "b"); + b.setProperty("lastName", "Johnson"); + datastore.put(ImmutableList.of(a, b)); + + try { + Entity result = retrievePersonWithLastName("Johnson"); + fail("Expected TooManyResultsException"); + } catch (TooManyResultsException expected) { + // TooManyResultsException does not provide addition details. + } + } + + // [START query_limit_example] + private List getTallestPeople() { + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + + Query q = new Query("Person").addSort("height", SortDirection.DESCENDING); + + PreparedQuery pq = datastore.prepare(q); + return pq.asList(FetchOptions.Builder.withLimit(5)); + } + // [END query_limit_example] + + @Test + public void queryLimitExample_returnsLimitedEntities() throws Exception { + Entity a = new Entity("Person", "a"); + a.setProperty("height", 200); + Entity b = new Entity("Person", "b"); + b.setProperty("height", 199); + Entity c = new Entity("Person", "c"); + c.setProperty("height", 201); + Entity d = new Entity("Person", "d"); + d.setProperty("height", 198); + Entity e = new Entity("Person", "e"); + e.setProperty("height", 202); + Entity f = new Entity("Person", "f"); + f.setProperty("height", 197); + Entity g = new Entity("Person", "g"); + g.setProperty("height", 203); + Entity h = new Entity("Person", "h"); + h.setProperty("height", 196); + datastore.put(ImmutableList.of(a, b, c, d, e, f, g, h)); + + List results = getTallestPeople(); + + assertThat(results).named("results").containsExactly(g, e, c, a, b).inOrder(); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/ReadPolicyTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/ReadPolicyTest.java new file mode 100644 index 00000000000..054088cd863 --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/ReadPolicyTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceConfig; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.ReadPolicy; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.common.collect.ImmutableList; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.List; + +/** + * Unit tests for {@link ReadPolicy}. + */ +@RunWith(JUnit4.class) +public class ReadPolicyTest { + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set 100% eventual consistency, so we can test with other job policies. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(100)); + + @Before + public void setUp() { + helper.setUp(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void readPolicy_eventual_returnsNoResults() { + // [START data_consistency] + double deadline = 5.0; + + // Construct a read policy for eventual consistency + ReadPolicy policy = new ReadPolicy(ReadPolicy.Consistency.EVENTUAL); + + // Set the read policy + DatastoreServiceConfig eventuallyConsistentConfig = + DatastoreServiceConfig.Builder.withReadPolicy(policy); + + // Set the call deadline + DatastoreServiceConfig deadlineConfig = DatastoreServiceConfig.Builder.withDeadline(deadline); + + // Set both the read policy and the call deadline + DatastoreServiceConfig datastoreConfig = + DatastoreServiceConfig.Builder.withReadPolicy(policy).deadline(deadline); + + // Get Datastore service with the given configuration + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(datastoreConfig); + // [END data_consistency] + + Entity parent = new Entity("Person", "a"); + Entity child = new Entity("Person", "b", parent.getKey()); + datastore.put(ImmutableList.of(parent, child)); + + // Even though we are using an ancestor query, the policy is set to + // eventual, so we should get eventually-consistent results. Since the + // local data store test config is set to 100% unapplied jobs, there + // should be no results. + Query q = new Query("Person").setAncestor(parent.getKey()); + List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").isEmpty(); + } + + @Test + public void readPolicy_strong_returnsAllResults() { + double deadline = 5.0; + ReadPolicy policy = new ReadPolicy(ReadPolicy.Consistency.STRONG); + DatastoreServiceConfig datastoreConfig = + DatastoreServiceConfig.Builder.withReadPolicy(policy).deadline(deadline); + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(datastoreConfig); + + Entity parent = new Entity("Person", "a"); + Entity child = new Entity("Person", "b", parent.getKey()); + datastore.put(ImmutableList.of(parent, child)); + + Query q = new Query("Person").setAncestor(parent.getKey()); + List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults()); + assertThat(results).named("query results").hasSize(2); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/StartupServletTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/StartupServletTest.java new file mode 100644 index 00000000000..c6876701c1a --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/StartupServletTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.Filter; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.datastore.Query.FilterPredicate; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link StartupServlet}. + */ +@RunWith(JUnit4.class) +public class StartupServletTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private DatastoreService datastore; + + private StartupServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + datastore = DatastoreServiceFactory.getDatastoreService(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new StartupServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_emptyDatastore_writesOkay() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + assertThat(responseWriter.toString()).named("StartupServlet response").isEqualTo("ok\n"); + } + + @Test + public void doGet_emptyDatastore_writesPresidents() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + Filter nameFilter = new FilterPredicate("name", FilterOperator.EQUAL, "George Washington"); + Query q = new Query("Person").setFilter(nameFilter); + Entity result = datastore.prepare(q).asSingleEntity(); + assertThat(result.getProperty("name")).named("name").isEqualTo("George Washington"); + } + + @Test + public void doGet_alreadyPopulated_writesOkay() throws Exception { + datastore.put( + new Entity(StartupServlet.IS_POPULATED_ENTITY, StartupServlet.IS_POPULATED_KEY_NAME)); + servletUnderTest.doGet(mockRequest, mockResponse); + assertThat(responseWriter.toString()).named("StartupServlet response").isEqualTo("ok\n"); + } +} diff --git a/appengine-java8/datastore/src/test/java/com/example/appengine/TransactionsTest.java b/appengine-java8/datastore/src/test/java/com/example/appengine/TransactionsTest.java new file mode 100644 index 00000000000..0b958cbf0da --- /dev/null +++ b/appengine-java8/datastore/src/test/java/com/example/appengine/TransactionsTest.java @@ -0,0 +1,309 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.EntityNotFoundException; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Transaction; +import com.google.appengine.api.datastore.TransactionOptions; +import com.google.appengine.api.taskqueue.Queue; +import com.google.appengine.api.taskqueue.QueueFactory; +import com.google.appengine.api.taskqueue.TaskOptions; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ConcurrentModificationException; +import java.util.Date; +import java.util.List; + +/** + * Unit tests to demonstrate App Engine Datastore transactions. + */ +@RunWith(JUnit4.class) +public class TransactionsTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Use High Rep job policy to allow cross group transactions in tests. + new LocalDatastoreServiceTestConfig().setApplyAllHighRepJobPolicy()); + + private DatastoreService datastore; + + @Before + public void setUp() { + helper.setUp(); + datastore = DatastoreServiceFactory.getDatastoreService(); + } + + @After + public void tearDown() { + // Clean up any dangling transactions. + Transaction txn = datastore.getCurrentTransaction(null); + if (txn != null && txn.isActive()) { + txn.rollback(); + } + helper.tearDown(); + } + + @Test + public void usingTransactions() throws Exception { + Entity joe = new Entity("Employee", "Joe"); + datastore.put(joe); + + // [START using_transactions] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + Transaction txn = datastore.beginTransaction(); + try { + Key employeeKey = KeyFactory.createKey("Employee", "Joe"); + Entity employee = datastore.get(employeeKey); + employee.setProperty("vacationDays", 10); + + datastore.put(txn, employee); + + txn.commit(); + } finally { + if (txn.isActive()) { + txn.rollback(); + } + } + // [END using_transactions] + } + + @Test + public void entityGroups() throws Exception { + try { + // [START entity_groups] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + Entity person = new Entity("Person", "tom"); + datastore.put(person); + + // Transactions on root entities + Transaction txn = datastore.beginTransaction(); + + Entity tom = datastore.get(person.getKey()); + tom.setProperty("age", 40); + datastore.put(txn, tom); + txn.commit(); + + // Transactions on child entities + txn = datastore.beginTransaction(); + tom = datastore.get(person.getKey()); + Entity photo = new Entity("Photo", tom.getKey()); + + // Create a Photo that is a child of the Person entity named "tom" + photo.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg"); + datastore.put(txn, photo); + txn.commit(); + + // Transactions on entities in different entity groups + txn = datastore.beginTransaction(); + tom = datastore.get(person.getKey()); + Entity photoNotAChild = new Entity("Photo"); + photoNotAChild.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg"); + datastore.put(txn, photoNotAChild); + + // Throws IllegalArgumentException because the Person entity + // and the Photo entity belong to different entity groups. + txn.commit(); + // [END entity_groups] + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // We expect to get an exception that complains that we don't have a XG-transaction. + } + } + + @Test + public void creatingAnEntityInASpecificEntityGroup() throws Exception { + String boardName = "my-message-board"; + + // [START creating_an_entity_in_a_specific_entity_group] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + + String messageTitle = "Some Title"; + String messageText = "Some message."; + Date postDate = new Date(); + + Transaction txn = datastore.beginTransaction(); + + Key messageBoardKey = KeyFactory.createKey("MessageBoard", boardName); + + Entity message = new Entity("Message", messageBoardKey); + message.setProperty("message_title", messageTitle); + message.setProperty("message_text", messageText); + message.setProperty("post_date", postDate); + datastore.put(txn, message); + + txn.commit(); + // [END creating_an_entity_in_a_specific_entity_group] + } + + @Test + public void crossGroupTransactions() throws Exception { + // [START cross-group_XG_transactions_using_the_Java_low-level_API] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + TransactionOptions options = TransactionOptions.Builder.withXG(true); + Transaction txn = datastore.beginTransaction(options); + + Entity a = new Entity("A"); + a.setProperty("a", 22); + datastore.put(txn, a); + + Entity b = new Entity("B"); + b.setProperty("b", 11); + datastore.put(txn, b); + + txn.commit(); + // [END cross-group_XG_transactions_using_the_Java_low-level_API] + } + + @Test + public void usesForTransactions_relativeUpdates() throws Exception { + String boardName = "my-message-board"; + Entity b = new Entity("MessageBoard", boardName); + b.setProperty("count", 41); + datastore.put(b); + + // [START uses_for_transactions_1] + int retries = 3; + while (true) { + Transaction txn = datastore.beginTransaction(); + try { + Key boardKey = KeyFactory.createKey("MessageBoard", boardName); + Entity messageBoard = datastore.get(boardKey); + + long count = (Long) messageBoard.getProperty("count"); + ++count; + messageBoard.setProperty("count", count); + datastore.put(txn, messageBoard); + + txn.commit(); + break; + } catch (ConcurrentModificationException e) { + if (retries == 0) { + throw e; + } + // Allow retry to occur + --retries; + } finally { + if (txn.isActive()) { + txn.rollback(); + } + } + } + // [END uses_for_transactions_1] + + b = datastore.get(KeyFactory.createKey("MessageBoard", boardName)); + assertThat((long) b.getProperty("count")).named("board.count").isEqualTo(42L); + } + + private Entity fetchOrCreate(String boardName) { + // [START uses_for_transactions_2] + Transaction txn = datastore.beginTransaction(); + Entity messageBoard; + Key boardKey; + try { + boardKey = KeyFactory.createKey("MessageBoard", boardName); + messageBoard = datastore.get(boardKey); + } catch (EntityNotFoundException e) { + messageBoard = new Entity("MessageBoard", boardName); + messageBoard.setProperty("count", 0L); + boardKey = datastore.put(txn, messageBoard); + } + txn.commit(); + // [END uses_for_transactions_2] + + return messageBoard; + } + + @Test + public void usesForTransactions_fetchOrCreate_fetchesExisting() throws Exception { + Entity b = new Entity("MessageBoard", "my-message-board"); + b.setProperty("count", 7); + datastore.put(b); + + Entity board = fetchOrCreate("my-message-board"); + + assertThat((long) board.getProperty("count")).named("board.count").isEqualTo(7L); + } + + @Test + public void usesForTransactions_fetchOrCreate_createsNew() throws Exception { + Entity board = fetchOrCreate("my-message-board"); + assertThat((long) board.getProperty("count")).named("board.count").isEqualTo(0L); + } + + @Test + public void usesForTransactions_readSnapshot() throws Exception { + String boardName = "my-message-board"; + Entity b = new Entity("MessageBoard", boardName); + b.setProperty("count", 13); + datastore.put(b); + + // [START uses_for_transactions_3] + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + + // Display information about a message board and its first 10 messages. + Key boardKey = KeyFactory.createKey("MessageBoard", boardName); + + Transaction txn = datastore.beginTransaction(); + + Entity messageBoard = datastore.get(boardKey); + long count = (Long) messageBoard.getProperty("count"); + + Query q = new Query("Message", boardKey); + + // This is an ancestor query. + PreparedQuery pq = datastore.prepare(txn, q); + List messages = pq.asList(FetchOptions.Builder.withLimit(10)); + + txn.commit(); + // [END uses_for_transactions_3] + + assertThat(count).named("board.count").isEqualTo(13L); + } + + @Test + public void transactionalTaskEnqueuing() throws Exception { + // [START transactional_task_enqueuing] + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + Queue queue = QueueFactory.getDefaultQueue(); + Transaction txn = datastore.beginTransaction(); + // ... + + queue.add(TaskOptions.Builder.withUrl("/path/to/handler")); + + // ... + + txn.commit(); + // [END transactional_task_enqueuing] + } +} diff --git a/appengine-java8/endpoints-frameworks-v2/README.md b/appengine-java8/endpoints-frameworks-v2/README.md new file mode 100644 index 00000000000..97f20f51bfc --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/README.md @@ -0,0 +1,19 @@ +# Google Cloud Endpoints Frameworks for App Engine Standard + +This directory contains Google Cloud Endpoints Frameworks for App Engine for +App Engine Standard samples. The [`backend/`](backend/) directory contains the +sample code for the [quickstart][4] for Cloud Endpoints Frameworks on App Engine +using an OpenAPI development process. The [`migration-example/`](migration-example/) directory +contains the sample code for a [migrated][2] version of the [Cloud Endpoints +Frameworks][1] sample project to the new [Cloud Endpoints Frameworks for App Engine][8] +development process. + +The new Google Cloud Endpoints Frameworks for App Engine provides +[additional functionality][3] using OpenAPI which may require payment. +It's recommended that you migrate projects using the prior version of [Cloud Endpoints Frameworks][1]. + + +[1]: https://cloud.google.com/appengine/docs/java/endpoints/ +[2]: https://cloud.google.com/appengine/docs/java/endpoints/migrating +[3]: https://cloud.google.com/endpoints/docs/frameworks/java/about-cloud-endpoints-frameworks +[4]: https://cloud.google.com/endpoints/docs/frameworks/java/quickstart-frameworks-java diff --git a/appengine-java8/endpoints-frameworks-v2/backend/.gitignore b/appengine-java8/endpoints-frameworks-v2/backend/.gitignore new file mode 100644 index 00000000000..efff06ec4bd --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/backend/.gitignore @@ -0,0 +1,2 @@ +openapi.json +swagger.json diff --git a/appengine-java8/endpoints-frameworks-v2/backend/README.md b/appengine-java8/endpoints-frameworks-v2/backend/README.md new file mode 100644 index 00000000000..4c226cda3b0 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/backend/README.md @@ -0,0 +1,72 @@ +# App Engine Standard & Google Cloud Endpoints Frameworks & Java + +This sample demonstrates how to use Google Cloud Endpoints Frameworks using +Java on App Engine Standard. + +## Adding the project ID to the sample API code + +You must add the project ID obtained when you created your project to the +sample's `pom.xml` before you can deploy the code. + +To add the project ID: + +0. Edit the file `pom.xml`. + +0. For ``, replace the value `YOUR_PROJECT_ID` with +your project ID. + +0. Edit the file `src/main/java/com/example/echo/Echo.java`. + +0. Replace the value `YOUR-PROJECT-ID` with your project ID. + +0. Save your changes. + +## Building the sample project + +To build the project: + + mvn clean package + +## Generating the openapi.json file + +To generate the required configuration file `openapi.json`: + + mvn exec:java -DGetSwaggerDoc + +## Deploying the sample API to App Engine + +To deploy the sample API: + +0. Invoke the `gcloud` command to deploy the API configuration file: + + gcloud service-management deploy openapi.json + +0. Deploy the API implementation code by invoking: + + mvn appengine:deploy + + The first time you upload a sample app, you may be prompted to authorize the + deployment. Follow the prompts: when you are presented with a browser window + containing a code, copy it to the terminal window. + +0. Wait for the upload to finish. + +## Sending a request to the sample API + +After you deploy the API and its configuration file, you can send requests +to the API. + +To send a request to the API, from a command line, invoke the following `cURL` +command: + + curl \ + -H "Content-Type: application/json" \ + -X POST \ + -d '{"message":"echo"}' \ + https://$PROJECT_ID.appspot.com/_ah/api/echo/v1/echo + +You will get a 200 response with the following data: + + { + "message": "echo" + } diff --git a/appengine-java8/endpoints-frameworks-v2/backend/pom.xml b/appengine-java8/endpoints-frameworks-v2/backend/pom.xml new file mode 100644 index 00000000000..376be68c122 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/backend/pom.xml @@ -0,0 +1,133 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + + com.example.echo + echo-j8 + + + appengine-java8-samples + com.google.cloud + 1.0.0 + ../.. + + + + UTF-8 + + 2.0.7 + 1.0.3 + + YOUR_PROJECT_ID + 1.8 + 1.8 + 1.3.1 + + + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + com.google.endpoints + endpoints-framework + ${endpoints.framework.version} + + + com.google.endpoints + endpoints-management-control-appengine-all + ${endpoints.management.version} + + + + + + GetSwaggerDoc + + + GetSwaggerDoc + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.4.0 + + true + com.google.api.server.spi.tools.EndpointsTool + + get-swagger-doc + --hostname=echo-api.endpoints.${endpoints.project.id}.cloud.goog + --war=target/echo-1.0-SNAPSHOT + com.example.echo.Echo + + + + + com.google.endpoints + endpoints-framework-tools + ${endpoints.framework.version} + + + com.google.appengine + appengine-api-1.0-sdk + 1.9.52 + + + + + + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + org.apache.maven.plugins + maven-war-plugin + 2.6 + + + + ${basedir}/src/main/webapp/WEB-INF + true + WEB-INF + + + + + + + com.google.cloud.tools + appengine-maven-plugin + ${appengine.maven.plugin} + + + + diff --git a/appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Echo.java b/appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Echo.java new file mode 100644 index 00000000000..5804e86dfca --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Echo.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2016 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.example.echo; + +import com.google.api.server.spi.auth.EspAuthenticator; +import com.google.api.server.spi.auth.common.User; +import com.google.api.server.spi.config.AnnotationBoolean; +import com.google.api.server.spi.config.Api; +import com.google.api.server.spi.config.ApiIssuer; +import com.google.api.server.spi.config.ApiIssuerAudience; +import com.google.api.server.spi.config.ApiMethod; +import com.google.api.server.spi.config.ApiNamespace; +import com.google.api.server.spi.config.Named; +import com.google.api.server.spi.config.Nullable; +import com.google.api.server.spi.response.UnauthorizedException; + +/** The Echo API which Endpoints will be exposing. */ +// [START echo_api_annotation] +@Api( + name = "echo", + version = "v1", + namespace = + @ApiNamespace( + ownerDomain = "echo.example.com", + ownerName = "echo.example.com", + packagePath = "" + ), + // [START_EXCLUDE] + issuers = { + @ApiIssuer( + name = "firebase", + issuer = "https://securetoken.google.com/YOUR-PROJECT-ID", + jwksUri = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com") + } + // [END_EXCLUDE] + ) +// [END echo_api_annotation] +public class Echo { + /** + * Echoes the received message back. If n is a non-negative integer, the message is copied that + * many times in the returned message. + * + * Note that name is specified and will override the default name of "{class name}.{method + * name}". For example, the default is "echo.echo". + * + * Note that httpMethod is not specified. This will default to a reasonable HTTP method + * depending on the API method name. In this case, the HTTP method will default to POST. + */ + // [START echo_method] + @ApiMethod(name = "echo") + public Message echo(Message message, @Named("n") @Nullable Integer n) { + return doEcho(message, n); + } + // [END echo_method] + + /** + * Echoes the received message back. If n is a non-negative integer, the message is copied that + * many times in the returned message. + * + * Note that name is specified and will override the default name of "{class name}.{method + * name}". For example, the default is "echo.echo". + * + * Note that httpMethod is not specified. This will default to a reasonable HTTP method + * depending on the API method name. In this case, the HTTP method will default to POST. + */ + // [START echo_path] + @ApiMethod(name = "echo_path_parameter", path = "echo/{n}") + public Message echoPathParameter(Message message, @Named("n") int n) { + return doEcho(message, n); + } + // [END echo_path] + + /** + * Echoes the received message back. If n is a non-negative integer, the message is copied that + * many times in the returned message. + * + * Note that name is specified and will override the default name of "{class name}.{method + * name}". For example, the default is "echo.echo". + * + * Note that httpMethod is not specified. This will default to a reasonable HTTP method + * depending on the API method name. In this case, the HTTP method will default to POST. + */ + // [START echo_api_key] + @ApiMethod(name = "echo_api_key", path = "echo_api_key", apiKeyRequired = AnnotationBoolean.TRUE) + public Message echoApiKey(Message message, @Named("n") @Nullable Integer n) { + return doEcho(message, n); + } + // [END echo_api_key] + + private Message doEcho(Message message, Integer n) { + if (n != null && n >= 0) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + if (i > 0) { + sb.append(" "); + } + sb.append(message.getMessage()); + } + message.setMessage(sb.toString()); + } + return message; + } + + /** + * Gets the authenticated user's email. If the user is not authenticated, this will return an HTTP + * 401. + * + * Note that name is not specified. This will default to "{class name}.{method name}". For + * example, the default is "echo.getUserEmail". + * + * Note that httpMethod is not required here. Without httpMethod, this will default to GET due + * to the API method name. httpMethod is added here for example purposes. + */ + // [START google_id_token_auth] + @ApiMethod( + httpMethod = ApiMethod.HttpMethod.GET, + authenticators = {EspAuthenticator.class}, + audiences = {"YOUR_OAUTH_CLIENT_ID"}, + clientIds = {"YOUR_OAUTH_CLIENT_ID"} + ) + public Email getUserEmail(User user) throws UnauthorizedException { + if (user == null) { + throw new UnauthorizedException("Invalid credentials"); + } + + Email response = new Email(); + response.setEmail(user.getEmail()); + return response; + } + // [END google_id_token_auth] + + /** + * Gets the authenticated user's email. If the user is not authenticated, this will return an HTTP + * 401. + * + * Note that name is not specified. This will default to "{class name}.{method name}". For + * example, the default is "echo.getUserEmail". + * + * Note that httpMethod is not required here. Without httpMethod, this will default to GET due + * to the API method name. httpMethod is added here for example purposes. + */ + // [START firebase_auth] + @ApiMethod( + path = "firebase_user", + httpMethod = ApiMethod.HttpMethod.GET, + authenticators = {EspAuthenticator.class}, + issuerAudiences = {@ApiIssuerAudience(name = "firebase", audiences = {"YOUR-PROJECT-ID"})} + ) + public Email getUserEmailFirebase(User user) throws UnauthorizedException { + if (user == null) { + throw new UnauthorizedException("Invalid credentials"); + } + + Email response = new Email(); + response.setEmail(user.getEmail()); + return response; + } + // [END firebase_auth] +} diff --git a/appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Email.java b/appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Email.java new file mode 100644 index 00000000000..e7725a9d9cc --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Email.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016 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.example.echo; + +/** The email bean that will be used in the getUserEmail response. */ +public class Email { + private String email; + + public String getEmail() { + return this.email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Message.java b/appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Message.java new file mode 100644 index 00000000000..64c043c8857 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/backend/src/main/java/com/example/echo/Message.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016 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.example.echo; + +/** The message bean that will be used in the echo request and response. */ +public class Message { + + private String message; + + public String getMessage() { + return this.message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..65f1329add0 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,32 @@ + + + + java8 + true + + + 2 + + + + + + + + + + diff --git a/appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..6279d0fef33 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,25 @@ +# Copyright 2016 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. + +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# + +# Set the default logging level for all loggers to WARNING +.level = WARNING diff --git a/appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/web.xml b/appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..f8d1d03f98a --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/backend/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,65 @@ + + + + + + EndpointsServlet + com.google.api.server.spi.EndpointsServlet + + services + com.example.echo.Echo + + + + + EndpointsServlet + /_ah/api/* + + + index.html + + + + + endpoints-api-configuration + com.google.api.control.ServiceManagementConfigFilter + + + + + endpoints-api-controller + com.google.api.control.extensions.appengine.GoogleAppEngineControlFilter + + endpoints.projectId + ${endpoints.project.id} + + + endpoints.serviceName + echo-api.endpoints.${endpoints.project.id}.cloud.goog + + + + + endpoints-api-configuration + EndpointsServlet + + + + endpoints-api-controller + EndpointsServlet + + diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/README.md b/appengine-java8/endpoints-frameworks-v2/migration-example/README.md new file mode 100644 index 00000000000..b9357a8af1c --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/README.md @@ -0,0 +1,125 @@ +# Hello World Google Cloud Endpoints for App Engine + +This sample provides an example of a [migration][7] from the prior version of +[Google Cloud Endpoints Frameworks][3] to new [Google Cloud Endpoints Frameworks for App Engine][8]. +This sample contains comments of how to use the prior Endpoints Frameworks as +well. For clarity, the prior Endpoints Frameworks and the new Endpoints +Frameworks are denoted as Endpoints Frameworks v1.0 and Endpoints Frameworks +v2.0, respectively. + +Google Cloud Endpoints Frameworks v2.0 provides new functionality which may +require payment and uses an OpenAPI specification. The OpenAPI development +process is explained [here][8] and a quickstart is provided [here][9]. + +## Products +- [Google App Engine Standard][1] + +## Language +- [Java][2] + +## APIs +- [Google Cloud Endpoints Frameworks v2.0][8] +- [Google Cloud Endpoints Frameworks v1.0][3] + +## Build and Deployment Plugins +- [Google Cloud Endpoints Frameworks Maven Plugin][10] +- [Google Cloud Endpoints Frameworks Gradle Plugin][11] + +## Setup +1. [Optional]: User Authenticating with Google Accounts in Web Clients + + 1. Update the `WEB_CLIENT_ID` in [Constants.java](src/main/java/com/example/helloendpoints/Constants.java) + to reflect the web client ID you have registered in the + [Credentials on Developers Console for OAuth 2.0 client IDs][6]. + + 1. Update the value of `google.devrel.samples.helloendpoints.CLIENT_ID` in + [base.js](src/main/webapp/js/base.js) to reflect the web client ID you + have registered in the + [Credentials on Developers Console for OAuth 2.0 client IDs][6]. + +1. [Optional]: User Authenticating with Google Accounts in other Applications Types + + - Inside [Constants.java](src/main/java/com/example/helloendpoints/Constants.java) you will find placeholders for Android + applications using Google Accounts client IDs registered in the + [Credentials on Developers Console for OAuth 2.0 client IDs][6]. + + - These client IDs are used when defining annotation for this sample API + found in [Greetings.java](src/main/java/com/example/helloendpoints/Greetings.java). + + - You can read more about different user authentication supported [here][12]. + + +1. [Optional]: Use Cloud Endpoints Frameworks v2.0 Maven and Gradle + client library generation plugins with Cloud Endpoints Frameworks v1.0. + + - Uncomment `Endpoints Frameworks v1.0` sections and comment + `Endpoints Frameworks v2.0` sections in the following files. + + ``` + pom.xml + build.gradle + src/main/webapp/WEB-INF/web.xml + ``` + +## Build and Deployment + +### Maven + +1. Build a fresh binary by using: + + `mvn clean compile` + +1. Run the application locally at [http://localhost:8080][5] by using: + + `mvn appengine:run` + +1. Explore local server's API explorer by browsing to: + + [http://localhost:8080/_ah/api/explorer][13] + +1. Generate the client library located at `target/client-libs/helloworld-v1-java.zip` + by using: + + `mvn endpoints-framework:clientLibs` + +1. Deploy your application to Google App Engine by using: + + `mvn appengine:deploy` + +### Gradle + +1. Build a fresh binary by using: + + `gradle clean compileJava` + +1. Run the application locally at [http://localhost:8080][5] by using: + + `gradle appengineRun` + +1. Explore local server's API explorer by browsing to: + + [http://localhost:8080/_ah/api/explorer][13] + +1. Generate the client library located at `build/endpointsClientLibs/helloworld-v1-java.zip` + by using: + + `gradle endpointsClientLibs` + +1. Deploy your application to Google App Engine by using: + + `gradle appengineDeploy` + + +[1]: https://cloud.google.com/appengine/docs/java/ +[2]: http://java.com/en/ +[3]: https://cloud.google.com/appengine/docs/java/endpoints/ +[4]: https://cloud.google.com/appengine/docs/java/tools/maven +[5]: http://localhost:8080/ +[6]: https://console.developers.google.com/project/_/apiui/credential +[7]: https://cloud.google.com/appengine/docs/java/endpoints/migrating +[8]: https://cloud.google.com/endpoints/docs/frameworks/java/about-cloud-endpoints-frameworks +[9]: https://cloud.google.com/endpoints/docs/frameworks/java/quickstart-frameworks-java +[10]: https://github.com/GoogleCloudPlatform/endpoints-framework-maven-plugin +[11]: https://github.com/GoogleCloudPlatform/endpoints-framework-gradle-plugin +[12]: https://cloud.google.com/endpoints/docs/authenticating-users-frameworks +[13]: http://localhost:8080/_ah/api/explorer diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/build.gradle b/appengine-java8/endpoints-frameworks-v2/migration-example/build.gradle new file mode 100644 index 00000000000..30b47fd9d20 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/build.gradle @@ -0,0 +1,84 @@ +// Copyright 2017 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. + +// [START buildscript] +buildscript { // Configuration for building + repositories { + mavenCentral() + jcenter() // Bintray's repository - a fast Maven Central mirror & more + } + dependencies { + // App Engine Gradle plugin + classpath 'com.google.cloud.tools:appengine-gradle-plugin:1.1.1' + + // Endpoints Frameworks Gradle plugin + classpath 'com.google.cloud.tools:endpoints-framework-gradle-plugin:1.0.0-beta6' + } +} +// [END buildscript] + +repositories { // repositories for Jar's you access in your code + mavenCentral() + jcenter() +} + +apply plugin: 'java' // standard Java tasks +apply plugin: 'war' // standard Web Archive plugin + +// [START apply_appengine] +apply plugin: 'com.google.cloud.tools.appengine' // App Engine tasks +// [END apply_appengine] + +// [START apply_endpoints-framework-server] +apply plugin: 'com.google.cloud.tools.endpoints-framework-server' +// [END apply_endpoints-framework-server] + +dependencies { + providedCompile group: 'javax.servlet', name: 'javax.servlet-api', version:'3.1.0' + compile 'jstl:jstl:1.2' + compile group: 'javax.inject', name: 'javax.inject', version: '1' + + // Uncomment to use Endpoints Frameworks v1.0 + // compile group: 'com.google.appengine', name: 'appengine-endpoints', version: '1.9.48' + // End of Endpoints Frameworks v1.0 + + // Endpoints Frameworks v2.0 + // [START endpoints-tools] + compile group: 'com.google.endpoints', name: 'endpoints-framework-tools', version: '2.0.4' + // [END endpoints-tools] + // End of Endpoints Frameworks v2.0 +} + +appengine { // App Engine tasks configuration + deploy { // deploy configuration + version = findProperty("appengine.deploy.version") + + def promoteProp = findProperty("appengine.deploy.promote") + if (promoteProp != null) { + promote = new Boolean(promoteProp) + } + } +} + +/* [START endpoints-server] +endpointsServer { + // Endpoints Framework Plugin server-side configuration +} +[END endpoints-server] */ + +group = 'com.example.helloendpoints' // Generated output GroupId +version = '1' // Version in generated output + +sourceCompatibility = 1.8 // App Engine Standard uses Java 7 +targetCompatibility = 1.8 // App Engine Standard uses Java 7 diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/gradle/wrapper/gradle-wrapper.properties b/appengine-java8/endpoints-frameworks-v2/migration-example/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..4a7d80cfe22 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Jan 16 22:18:59 PST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-bin.zip diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/gradlew b/appengine-java8/endpoints-frameworks-v2/migration-example/gradlew new file mode 100755 index 00000000000..4453ccea33d --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/gradlew.bat b/appengine-java8/endpoints-frameworks-v2/migration-example/gradlew.bat new file mode 100644 index 00000000000..e95643d6a2c --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/jenkins.sh b/appengine-java8/endpoints-frameworks-v2/migration-example/jenkins.sh new file mode 100755 index 00000000000..3bc1515636c --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/jenkins.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Copyright 2017 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. + +# Fail on non-zero return and print command to stdout +set -xe + +# Jenkins Test Script +function TestEndpoints () { + # Test getGreeting Endpoint (hello world!) + curl -X GET \ + "https://${2}-dot-${1}.appspot.com/_ah/api/helloworld/v1/hellogreeting/0" | \ + tee "$ERROR_OUTPUT_DIR/response.json" | \ + grep "hello version-${2}" + + # Test getGreeting Endpoint (goodbye world!) + curl -X GET \ + "https://${2}-dot-${1}.appspot.com/_ah/api/helloworld/v1/hellogreeting/1" | \ + tee "$ERROR_OUTPUT_DIR/response.json" | \ + grep "goodbye world!" + + # Test listGreeting Endpoint (hello world! and goodbye world!) + curl -X GET \ + "https://${2}-dot-${1}.appspot.com/_ah/api/helloworld/v1/hellogreeting" | \ + tee "$ERROR_OUTPUT_DIR/response.json" | \ + grep "hello world!\|goodbye world!" + + # Test multiply Endpoint (This is a greeting.) + curl -X POST \ + -H "Content-Type: application/json" \ + --data "{'message':'This is a greeting from instance ${2}'}." \ + "https://${2}-dot-${1}.appspot.com/_ah/api/helloworld/v1/hellogreeting/1" | \ + tee "$ERROR_OUTPUT_DIR/response.json" | \ + grep "This is a greeting from instance ${2}." +} + +# Jenkins provides values for GOOGLE_PROJECT_ID and GOOGLE_VERSION_ID +# Update Greetings.java +sed -i'.bak' -e "s/hello world!/hello version-${GOOGLE_VERSION_ID}!/g" src/main/java/com/example/helloendpoints/Greetings.java + +# Test with Maven +mvn clean appengine:deploy \ + -Dapp.deploy.version="${GOOGLE_VERSION_ID}" \ + -Dapp.deploy.promote=false + +# End-2-End tests +TestEndpoints "${GOOGLE_PROJECT_ID}" "${GOOGLE_VERSION_ID}" + +# Clean +mvn clean + +# Test with Gradle +# Modify Greetings.java for Gradle +sed -i'.bak' -e "s/hello version-${GOOGLE_VERSION_ID}!/hello version-${GOOGLE_VERSION_ID}!/g" src/main/java/com/example/helloendpoints/Greetings.java + +# Deploy Gradle +gradle -Pappengine.deploy.promote=false \ + -Pappengine.deploy.version="${GOOGLE_VERSION_ID}" \ + appengineDeploy + +# End-2-End tests +TestEndpoints "${GOOGLE_PROJECT_ID}" "${GOOGLE_VERSION_ID}" + +# Clean +gradle clean diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/pom.xml b/appengine-java8/endpoints-frameworks-v2/migration-example/pom.xml new file mode 100644 index 00000000000..5694301ea67 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/pom.xml @@ -0,0 +1,116 @@ + + + + 4.0.0 + war + 1.0-SNAPSHOT + + com.example.helloendpoints + helloendpoints-j8 + + + appengine-java8-samples + com.google.cloud + 1.0.0 + ../.. + + + + 1 + 1.8 + 1.8 + 2.1 + UTF-8 + + + + + + + + + + + com.google.endpoints + endpoints-framework + 2.0.4 + + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + javax.inject + javax.inject + ${javax.inject.version} + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + + + + + + + com.google.cloud.tools + endpoints-framework-maven-plugin + 1.0.0-beta4 + + + + + + + org.codehaus.mojo + versions-maven-plugin + ${mojo.versions.maven.version} + + + compile + + display-dependency-updates + display-plugin-updates + + + + + + + diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/Constants.java b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/Constants.java new file mode 100644 index 00000000000..327b7e290ac --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/Constants.java @@ -0,0 +1,28 @@ +/* +Copyright 2017 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.example.helloendpoints; + +/** + * Contains the client IDs and scopes for allowed clients consuming the helloworld API. + */ +public class Constants { + public static final String WEB_CLIENT_ID = "replace this with your web client ID"; + public static final String ANDROID_CLIENT_ID = "replace this with your Android client ID"; + public static final String ANDROID_AUDIENCE = WEB_CLIENT_ID; + + public static final String EMAIL_SCOPE = "https://www.googleapis.com/auth/userinfo.email"; +} diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/Greetings.java b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/Greetings.java new file mode 100644 index 00000000000..4bda5c883b4 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/Greetings.java @@ -0,0 +1,88 @@ +/* +Copyright 2017 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. +*/ + +//[START begin] +package com.example.helloendpoints; + +import com.google.api.server.spi.config.Api; +import com.google.api.server.spi.config.ApiMethod; +import com.google.api.server.spi.response.NotFoundException; +import com.google.appengine.api.users.User; + +import java.util.ArrayList; + +import javax.inject.Named; +//[END begin] + +//[START api_def] + +/** + * Defines v1 of a helloworld API, which provides simple "greeting" methods. + */ +@Api(name = "helloworld", + version = "v1", + scopes = {Constants.EMAIL_SCOPE}, + clientIds = {Constants.WEB_CLIENT_ID, Constants.ANDROID_CLIENT_ID}, + audiences = {Constants.ANDROID_AUDIENCE} + ) +public class Greetings { + + public static ArrayList greetings = new ArrayList(); + + static { + greetings.add(new HelloGreeting("hello world!")); + greetings.add(new HelloGreeting("goodbye world!")); + } +//[END api_def] + +//[START getgreetings] + + public HelloGreeting getGreeting(@Named("id") Integer id) throws NotFoundException { + try { + return greetings.get(id); + } catch (IndexOutOfBoundsException e) { + throw new NotFoundException("Greeting not found with an index: " + id); + } + } + + public ArrayList listGreeting() { + return greetings; + } +//[END getgreetings] + +//[START multiplygreetings] + + @ApiMethod(name = "greetings.multiply", httpMethod = "post") + public HelloGreeting insertGreeting(@Named("times") Integer times, HelloGreeting greeting) { + HelloGreeting response = new HelloGreeting(); + StringBuilder responseBuilder = new StringBuilder(); + for (int i = 0; i < times; i++) { + responseBuilder.append(greeting.getMessage()); + } + response.setMessage(responseBuilder.toString()); + return response; + } +//[END multiplygreetings] + +//[START auth] + + @ApiMethod(name = "greetings.authed", path = "hellogreeting/authed") + public HelloGreeting authedGreeting(User user) { + HelloGreeting response = new HelloGreeting("hello " + user.getEmail()); + return response; + } +//[END auth] +} diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/HelloGreeting.java b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/HelloGreeting.java new file mode 100644 index 00000000000..7cf19f7c482 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/java/com/example/helloendpoints/HelloGreeting.java @@ -0,0 +1,36 @@ +/* +Copyright 2017 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.example.helloendpoints; + +public class HelloGreeting { + + public String message; + + public HelloGreeting() {} + + public HelloGreeting(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..e81085b6dde --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,23 @@ + + + + java8 + true + + + + diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..a17206681f0 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,13 @@ +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# + +# Set the default logging level for all loggers to WARNING +.level = WARNING diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/web.xml b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..be7073d2745 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,64 @@ + + + + + + + + + + EndpointsServlet + com.google.api.server.spi.EndpointsServlet + + services + com.example.helloendpoints.Greetings + + + restricted + false + + + + EndpointsServlet + /_ah/api/* + + + + + + index.html + + diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/bootstrap/css/bootstrap-responsive.css b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/bootstrap/css/bootstrap-responsive.css new file mode 100644 index 00000000000..09e88ce3fec --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/bootstrap/css/bootstrap-responsive.css @@ -0,0 +1,1109 @@ +/*! + * Bootstrap Responsive v2.3.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +@-ms-viewport { + width: device-width; +} + +.hidden { + display: none; + visibility: hidden; +} + +.visible-phone { + display: none !important; +} + +.visible-tablet { + display: none !important; +} + +.hidden-desktop { + display: none !important; +} + +.visible-desktop { + display: inherit !important; +} + +@media (min-width: 768px) and (max-width: 979px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important ; + } + .visible-tablet { + display: inherit !important; + } + .hidden-tablet { + display: none !important; + } +} + +@media (max-width: 767px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important; + } + .visible-phone { + display: inherit !important; + } + .hidden-phone { + display: none !important; + } +} + +.visible-print { + display: none !important; +} + +@media print { + .visible-print { + display: inherit !important; + } + .hidden-print { + display: none !important; + } +} + +@media (min-width: 1200px) { + .row { + margin-left: -30px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 30px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 1170px; + } + .span12 { + width: 1170px; + } + .span11 { + width: 1070px; + } + .span10 { + width: 970px; + } + .span9 { + width: 870px; + } + .span8 { + width: 770px; + } + .span7 { + width: 670px; + } + .span6 { + width: 570px; + } + .span5 { + width: 470px; + } + .span4 { + width: 370px; + } + .span3 { + width: 270px; + } + .span2 { + width: 170px; + } + .span1 { + width: 70px; + } + .offset12 { + margin-left: 1230px; + } + .offset11 { + margin-left: 1130px; + } + .offset10 { + margin-left: 1030px; + } + .offset9 { + margin-left: 930px; + } + .offset8 { + margin-left: 830px; + } + .offset7 { + margin-left: 730px; + } + .offset6 { + margin-left: 630px; + } + .offset5 { + margin-left: 530px; + } + .offset4 { + margin-left: 430px; + } + .offset3 { + margin-left: 330px; + } + .offset2 { + margin-left: 230px; + } + .offset1 { + margin-left: 130px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.564102564102564%; + *margin-left: 2.5109110747408616%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.564102564102564%; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.45299145299145%; + *width: 91.39979996362975%; + } + .row-fluid .span10 { + width: 82.90598290598291%; + *width: 82.8527914166212%; + } + .row-fluid .span9 { + width: 74.35897435897436%; + *width: 74.30578286961266%; + } + .row-fluid .span8 { + width: 65.81196581196582%; + *width: 65.75877432260411%; + } + .row-fluid .span7 { + width: 57.26495726495726%; + *width: 57.21176577559556%; + } + .row-fluid .span6 { + width: 48.717948717948715%; + *width: 48.664757228587014%; + } + .row-fluid .span5 { + width: 40.17094017094017%; + *width: 40.11774868157847%; + } + .row-fluid .span4 { + width: 31.623931623931625%; + *width: 31.570740134569924%; + } + .row-fluid .span3 { + width: 23.076923076923077%; + *width: 23.023731587561375%; + } + .row-fluid .span2 { + width: 14.52991452991453%; + *width: 14.476723040552828%; + } + .row-fluid .span1 { + width: 5.982905982905983%; + *width: 5.929714493544281%; + } + .row-fluid .offset12 { + margin-left: 105.12820512820512%; + *margin-left: 105.02182214948171%; + } + .row-fluid .offset12:first-child { + margin-left: 102.56410256410257%; + *margin-left: 102.45771958537915%; + } + .row-fluid .offset11 { + margin-left: 96.58119658119658%; + *margin-left: 96.47481360247316%; + } + .row-fluid .offset11:first-child { + margin-left: 94.01709401709402%; + *margin-left: 93.91071103837061%; + } + .row-fluid .offset10 { + margin-left: 88.03418803418803%; + *margin-left: 87.92780505546462%; + } + .row-fluid .offset10:first-child { + margin-left: 85.47008547008548%; + *margin-left: 85.36370249136206%; + } + .row-fluid .offset9 { + margin-left: 79.48717948717949%; + *margin-left: 79.38079650845607%; + } + .row-fluid .offset9:first-child { + margin-left: 76.92307692307693%; + *margin-left: 76.81669394435352%; + } + .row-fluid .offset8 { + margin-left: 70.94017094017094%; + *margin-left: 70.83378796144753%; + } + .row-fluid .offset8:first-child { + margin-left: 68.37606837606839%; + *margin-left: 68.26968539734497%; + } + .row-fluid .offset7 { + margin-left: 62.393162393162385%; + *margin-left: 62.28677941443899%; + } + .row-fluid .offset7:first-child { + margin-left: 59.82905982905982%; + *margin-left: 59.72267685033642%; + } + .row-fluid .offset6 { + margin-left: 53.84615384615384%; + *margin-left: 53.739770867430444%; + } + .row-fluid .offset6:first-child { + margin-left: 51.28205128205128%; + *margin-left: 51.175668303327875%; + } + .row-fluid .offset5 { + margin-left: 45.299145299145295%; + *margin-left: 45.1927623204219%; + } + .row-fluid .offset5:first-child { + margin-left: 42.73504273504273%; + *margin-left: 42.62865975631933%; + } + .row-fluid .offset4 { + margin-left: 36.75213675213675%; + *margin-left: 36.645753773413354%; + } + .row-fluid .offset4:first-child { + margin-left: 34.18803418803419%; + *margin-left: 34.081651209310785%; + } + .row-fluid .offset3 { + margin-left: 28.205128205128204%; + *margin-left: 28.0987452264048%; + } + .row-fluid .offset3:first-child { + margin-left: 25.641025641025642%; + *margin-left: 25.53464266230224%; + } + .row-fluid .offset2 { + margin-left: 19.65811965811966%; + *margin-left: 19.551736679396257%; + } + .row-fluid .offset2:first-child { + margin-left: 17.094017094017094%; + *margin-left: 16.98763411529369%; + } + .row-fluid .offset1 { + margin-left: 11.11111111111111%; + *margin-left: 11.004728132387708%; + } + .row-fluid .offset1:first-child { + margin-left: 8.547008547008547%; + *margin-left: 8.440625568285142%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 30px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 1156px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 1056px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 956px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 856px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 756px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 656px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 556px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 456px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 356px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 256px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 156px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 56px; + } + .thumbnails { + margin-left: -30px; + } + .thumbnails > li { + margin-left: 30px; + } + .row-fluid .thumbnails { + margin-left: 0; + } +} + +@media (min-width: 768px) and (max-width: 979px) { + .row { + margin-left: -20px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 724px; + } + .span12 { + width: 724px; + } + .span11 { + width: 662px; + } + .span10 { + width: 600px; + } + .span9 { + width: 538px; + } + .span8 { + width: 476px; + } + .span7 { + width: 414px; + } + .span6 { + width: 352px; + } + .span5 { + width: 290px; + } + .span4 { + width: 228px; + } + .span3 { + width: 166px; + } + .span2 { + width: 104px; + } + .span1 { + width: 42px; + } + .offset12 { + margin-left: 764px; + } + .offset11 { + margin-left: 702px; + } + .offset10 { + margin-left: 640px; + } + .offset9 { + margin-left: 578px; + } + .offset8 { + margin-left: 516px; + } + .offset7 { + margin-left: 454px; + } + .offset6 { + margin-left: 392px; + } + .offset5 { + margin-left: 330px; + } + .offset4 { + margin-left: 268px; + } + .offset3 { + margin-left: 206px; + } + .offset2 { + margin-left: 144px; + } + .offset1 { + margin-left: 82px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.7624309392265194%; + *margin-left: 2.709239449864817%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.7624309392265194%; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.43646408839778%; + *width: 91.38327259903608%; + } + .row-fluid .span10 { + width: 82.87292817679558%; + *width: 82.81973668743387%; + } + .row-fluid .span9 { + width: 74.30939226519337%; + *width: 74.25620077583166%; + } + .row-fluid .span8 { + width: 65.74585635359117%; + *width: 65.69266486422946%; + } + .row-fluid .span7 { + width: 57.18232044198895%; + *width: 57.12912895262725%; + } + .row-fluid .span6 { + width: 48.61878453038674%; + *width: 48.56559304102504%; + } + .row-fluid .span5 { + width: 40.05524861878453%; + *width: 40.00205712942283%; + } + .row-fluid .span4 { + width: 31.491712707182323%; + *width: 31.43852121782062%; + } + .row-fluid .span3 { + width: 22.92817679558011%; + *width: 22.87498530621841%; + } + .row-fluid .span2 { + width: 14.3646408839779%; + *width: 14.311449394616199%; + } + .row-fluid .span1 { + width: 5.801104972375691%; + *width: 5.747913483013988%; + } + .row-fluid .offset12 { + margin-left: 105.52486187845304%; + *margin-left: 105.41847889972962%; + } + .row-fluid .offset12:first-child { + margin-left: 102.76243093922652%; + *margin-left: 102.6560479605031%; + } + .row-fluid .offset11 { + margin-left: 96.96132596685082%; + *margin-left: 96.8549429881274%; + } + .row-fluid .offset11:first-child { + margin-left: 94.1988950276243%; + *margin-left: 94.09251204890089%; + } + .row-fluid .offset10 { + margin-left: 88.39779005524862%; + *margin-left: 88.2914070765252%; + } + .row-fluid .offset10:first-child { + margin-left: 85.6353591160221%; + *margin-left: 85.52897613729868%; + } + .row-fluid .offset9 { + margin-left: 79.8342541436464%; + *margin-left: 79.72787116492299%; + } + .row-fluid .offset9:first-child { + margin-left: 77.07182320441989%; + *margin-left: 76.96544022569647%; + } + .row-fluid .offset8 { + margin-left: 71.2707182320442%; + *margin-left: 71.16433525332079%; + } + .row-fluid .offset8:first-child { + margin-left: 68.50828729281768%; + *margin-left: 68.40190431409427%; + } + .row-fluid .offset7 { + margin-left: 62.70718232044199%; + *margin-left: 62.600799341718584%; + } + .row-fluid .offset7:first-child { + margin-left: 59.94475138121547%; + *margin-left: 59.838368402492065%; + } + .row-fluid .offset6 { + margin-left: 54.14364640883978%; + *margin-left: 54.037263430116376%; + } + .row-fluid .offset6:first-child { + margin-left: 51.38121546961326%; + *margin-left: 51.27483249088986%; + } + .row-fluid .offset5 { + margin-left: 45.58011049723757%; + *margin-left: 45.47372751851417%; + } + .row-fluid .offset5:first-child { + margin-left: 42.81767955801105%; + *margin-left: 42.71129657928765%; + } + .row-fluid .offset4 { + margin-left: 37.01657458563536%; + *margin-left: 36.91019160691196%; + } + .row-fluid .offset4:first-child { + margin-left: 34.25414364640884%; + *margin-left: 34.14776066768544%; + } + .row-fluid .offset3 { + margin-left: 28.45303867403315%; + *margin-left: 28.346655695309746%; + } + .row-fluid .offset3:first-child { + margin-left: 25.69060773480663%; + *margin-left: 25.584224756083227%; + } + .row-fluid .offset2 { + margin-left: 19.88950276243094%; + *margin-left: 19.783119783707537%; + } + .row-fluid .offset2:first-child { + margin-left: 17.12707182320442%; + *margin-left: 17.02068884448102%; + } + .row-fluid .offset1 { + margin-left: 11.32596685082873%; + *margin-left: 11.219583872105325%; + } + .row-fluid .offset1:first-child { + margin-left: 8.56353591160221%; + *margin-left: 8.457152932878806%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 710px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 648px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 586px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 524px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 462px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 400px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 338px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 276px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 214px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 152px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 90px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 28px; + } +} + +@media (max-width: 767px) { + body { + padding-right: 20px; + padding-left: 20px; + } + .navbar-fixed-top, + .navbar-fixed-bottom, + .navbar-static-top { + margin-right: -20px; + margin-left: -20px; + } + .container-fluid { + padding: 0; + } + .dl-horizontal dt { + float: none; + width: auto; + clear: none; + text-align: left; + } + .dl-horizontal dd { + margin-left: 0; + } + .container { + width: auto; + } + .row-fluid { + width: 100%; + } + .row, + .thumbnails { + margin-left: 0; + } + .thumbnails > li { + float: none; + margin-left: 0; + } + [class*="span"], + .uneditable-input[class*="span"], + .row-fluid [class*="span"] { + display: block; + float: none; + width: 100%; + margin-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .span12, + .row-fluid .span12 { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="offset"]:first-child { + margin-left: 0; + } + .input-large, + .input-xlarge, + .input-xxlarge, + input[class*="span"], + select[class*="span"], + textarea[class*="span"], + .uneditable-input { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .input-prepend input, + .input-append input, + .input-prepend input[class*="span"], + .input-append input[class*="span"] { + display: inline-block; + width: auto; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 0; + } + .modal { + position: fixed; + top: 20px; + right: 20px; + left: 20px; + width: auto; + margin: 0; + } + .modal.fade { + top: -100px; + } + .modal.fade.in { + top: 20px; + } +} + +@media (max-width: 480px) { + .nav-collapse { + -webkit-transform: translate3d(0, 0, 0); + } + .page-header h1 small { + display: block; + line-height: 20px; + } + input[type="checkbox"], + input[type="radio"] { + border: 1px solid #ccc; + } + .form-horizontal .control-label { + float: none; + width: auto; + padding-top: 0; + text-align: left; + } + .form-horizontal .controls { + margin-left: 0; + } + .form-horizontal .control-list { + padding-top: 0; + } + .form-horizontal .form-actions { + padding-right: 10px; + padding-left: 10px; + } + .media .pull-left, + .media .pull-right { + display: block; + float: none; + margin-bottom: 10px; + } + .media-object { + margin-right: 0; + margin-left: 0; + } + .modal { + top: 10px; + right: 10px; + left: 10px; + } + .modal-header .close { + padding: 10px; + margin: -10px; + } + .carousel-caption { + position: static; + } +} + +@media (max-width: 979px) { + body { + padding-top: 0; + } + .navbar-fixed-top, + .navbar-fixed-bottom { + position: static; + } + .navbar-fixed-top { + margin-bottom: 20px; + } + .navbar-fixed-bottom { + margin-top: 20px; + } + .navbar-fixed-top .navbar-inner, + .navbar-fixed-bottom .navbar-inner { + padding: 5px; + } + .navbar .container { + width: auto; + padding: 0; + } + .navbar .brand { + padding-right: 10px; + padding-left: 10px; + margin: 0 0 0 -5px; + } + .nav-collapse { + clear: both; + } + .nav-collapse .nav { + float: none; + margin: 0 0 10px; + } + .nav-collapse .nav > li { + float: none; + } + .nav-collapse .nav > li > a { + margin-bottom: 2px; + } + .nav-collapse .nav > .divider-vertical { + display: none; + } + .nav-collapse .nav .nav-header { + color: #777777; + text-shadow: none; + } + .nav-collapse .nav > li > a, + .nav-collapse .dropdown-menu a { + padding: 9px 15px; + font-weight: bold; + color: #777777; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + .nav-collapse .btn { + padding: 4px 10px 4px; + font-weight: normal; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + } + .nav-collapse .dropdown-menu li + li a { + margin-bottom: 2px; + } + .nav-collapse .nav > li > a:hover, + .nav-collapse .nav > li > a:focus, + .nav-collapse .dropdown-menu a:hover, + .nav-collapse .dropdown-menu a:focus { + background-color: #f2f2f2; + } + .navbar-inverse .nav-collapse .nav > li > a, + .navbar-inverse .nav-collapse .dropdown-menu a { + color: #999999; + } + .navbar-inverse .nav-collapse .nav > li > a:hover, + .navbar-inverse .nav-collapse .nav > li > a:focus, + .navbar-inverse .nav-collapse .dropdown-menu a:hover, + .navbar-inverse .nav-collapse .dropdown-menu a:focus { + background-color: #111111; + } + .nav-collapse.in .btn-group { + padding: 0; + margin-top: 5px; + } + .nav-collapse .dropdown-menu { + position: static; + top: auto; + left: auto; + display: none; + float: none; + max-width: none; + padding: 0; + margin: 0 15px; + background-color: transparent; + border: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } + .nav-collapse .open > .dropdown-menu { + display: block; + } + .nav-collapse .dropdown-menu:before, + .nav-collapse .dropdown-menu:after { + display: none; + } + .nav-collapse .dropdown-menu .divider { + display: none; + } + .nav-collapse .nav > li > .dropdown-menu:before, + .nav-collapse .nav > li > .dropdown-menu:after { + display: none; + } + .nav-collapse .navbar-form, + .nav-collapse .navbar-search { + float: none; + padding: 10px 15px; + margin: 10px 0; + border-top: 1px solid #f2f2f2; + border-bottom: 1px solid #f2f2f2; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar-inverse .nav-collapse .navbar-form, + .navbar-inverse .nav-collapse .navbar-search { + border-top-color: #111111; + border-bottom-color: #111111; + } + .navbar .nav-collapse .nav.pull-right { + float: none; + margin-left: 0; + } + .nav-collapse, + .nav-collapse.collapse { + height: 0; + overflow: hidden; + } + .navbar .btn-navbar { + display: block; + } + .navbar-static .navbar-inner { + padding-right: 10px; + padding-left: 10px; + } +} + +@media (min-width: 980px) { + .nav-collapse.collapse { + height: auto !important; + overflow: visible !important; + } +} diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/bootstrap/css/bootstrap.css b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/bootstrap/css/bootstrap.css new file mode 100644 index 00000000000..b725064aab2 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/bootstrap/css/bootstrap.css @@ -0,0 +1,6167 @@ +/*! + * Bootstrap v2.3.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +audio:not([controls]) { + display: none; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:hover, +a:active { + outline: 0; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + width: auto\9; + height: auto; + max-width: 100%; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} + +#map_canvas img, +.google-maps img { + max-width: none; +} + +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} + +button, +input { + *overflow: visible; + line-height: normal; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +label, +select, +button, +input[type="button"], +input[type="reset"], +input[type="submit"], +input[type="radio"], +input[type="checkbox"] { + cursor: pointer; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +@media print { + * { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + @page { + margin: 0.5cm; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } +} + +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 20px; + color: #333333; + background-color: #ffffff; +} + +a { + color: #0088cc; + text-decoration: none; +} + +a:hover, +a:focus { + color: #005580; + text-decoration: underline; +} + +.img-rounded { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.img-polaroid { + padding: 4px; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.img-circle { + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + border-radius: 500px; +} + +.row { + margin-left: -20px; + *zoom: 1; +} + +.row:before, +.row:after { + display: table; + line-height: 0; + content: ""; +} + +.row:after { + clear: both; +} + +[class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; +} + +.container, +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.span12 { + width: 940px; +} + +.span11 { + width: 860px; +} + +.span10 { + width: 780px; +} + +.span9 { + width: 700px; +} + +.span8 { + width: 620px; +} + +.span7 { + width: 540px; +} + +.span6 { + width: 460px; +} + +.span5 { + width: 380px; +} + +.span4 { + width: 300px; +} + +.span3 { + width: 220px; +} + +.span2 { + width: 140px; +} + +.span1 { + width: 60px; +} + +.offset12 { + margin-left: 980px; +} + +.offset11 { + margin-left: 900px; +} + +.offset10 { + margin-left: 820px; +} + +.offset9 { + margin-left: 740px; +} + +.offset8 { + margin-left: 660px; +} + +.offset7 { + margin-left: 580px; +} + +.offset6 { + margin-left: 500px; +} + +.offset5 { + margin-left: 420px; +} + +.offset4 { + margin-left: 340px; +} + +.offset3 { + margin-left: 260px; +} + +.offset2 { + margin-left: 180px; +} + +.offset1 { + margin-left: 100px; +} + +.row-fluid { + width: 100%; + *zoom: 1; +} + +.row-fluid:before, +.row-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.row-fluid:after { + clear: both; +} + +.row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.127659574468085%; + *margin-left: 2.074468085106383%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.row-fluid [class*="span"]:first-child { + margin-left: 0; +} + +.row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.127659574468085%; +} + +.row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; +} + +.row-fluid .span11 { + width: 91.48936170212765%; + *width: 91.43617021276594%; +} + +.row-fluid .span10 { + width: 82.97872340425532%; + *width: 82.92553191489361%; +} + +.row-fluid .span9 { + width: 74.46808510638297%; + *width: 74.41489361702126%; +} + +.row-fluid .span8 { + width: 65.95744680851064%; + *width: 65.90425531914893%; +} + +.row-fluid .span7 { + width: 57.44680851063829%; + *width: 57.39361702127659%; +} + +.row-fluid .span6 { + width: 48.93617021276595%; + *width: 48.88297872340425%; +} + +.row-fluid .span5 { + width: 40.42553191489362%; + *width: 40.37234042553192%; +} + +.row-fluid .span4 { + width: 31.914893617021278%; + *width: 31.861702127659576%; +} + +.row-fluid .span3 { + width: 23.404255319148934%; + *width: 23.351063829787233%; +} + +.row-fluid .span2 { + width: 14.893617021276595%; + *width: 14.840425531914894%; +} + +.row-fluid .span1 { + width: 6.382978723404255%; + *width: 6.329787234042553%; +} + +.row-fluid .offset12 { + margin-left: 104.25531914893617%; + *margin-left: 104.14893617021275%; +} + +.row-fluid .offset12:first-child { + margin-left: 102.12765957446808%; + *margin-left: 102.02127659574467%; +} + +.row-fluid .offset11 { + margin-left: 95.74468085106382%; + *margin-left: 95.6382978723404%; +} + +.row-fluid .offset11:first-child { + margin-left: 93.61702127659574%; + *margin-left: 93.51063829787232%; +} + +.row-fluid .offset10 { + margin-left: 87.23404255319149%; + *margin-left: 87.12765957446807%; +} + +.row-fluid .offset10:first-child { + margin-left: 85.1063829787234%; + *margin-left: 84.99999999999999%; +} + +.row-fluid .offset9 { + margin-left: 78.72340425531914%; + *margin-left: 78.61702127659572%; +} + +.row-fluid .offset9:first-child { + margin-left: 76.59574468085106%; + *margin-left: 76.48936170212764%; +} + +.row-fluid .offset8 { + margin-left: 70.2127659574468%; + *margin-left: 70.10638297872339%; +} + +.row-fluid .offset8:first-child { + margin-left: 68.08510638297872%; + *margin-left: 67.9787234042553%; +} + +.row-fluid .offset7 { + margin-left: 61.70212765957446%; + *margin-left: 61.59574468085106%; +} + +.row-fluid .offset7:first-child { + margin-left: 59.574468085106375%; + *margin-left: 59.46808510638297%; +} + +.row-fluid .offset6 { + margin-left: 53.191489361702125%; + *margin-left: 53.085106382978715%; +} + +.row-fluid .offset6:first-child { + margin-left: 51.063829787234035%; + *margin-left: 50.95744680851063%; +} + +.row-fluid .offset5 { + margin-left: 44.68085106382979%; + *margin-left: 44.57446808510638%; +} + +.row-fluid .offset5:first-child { + margin-left: 42.5531914893617%; + *margin-left: 42.4468085106383%; +} + +.row-fluid .offset4 { + margin-left: 36.170212765957444%; + *margin-left: 36.06382978723405%; +} + +.row-fluid .offset4:first-child { + margin-left: 34.04255319148936%; + *margin-left: 33.93617021276596%; +} + +.row-fluid .offset3 { + margin-left: 27.659574468085104%; + *margin-left: 27.5531914893617%; +} + +.row-fluid .offset3:first-child { + margin-left: 25.53191489361702%; + *margin-left: 25.425531914893618%; +} + +.row-fluid .offset2 { + margin-left: 19.148936170212764%; + *margin-left: 19.04255319148936%; +} + +.row-fluid .offset2:first-child { + margin-left: 17.02127659574468%; + *margin-left: 16.914893617021278%; +} + +.row-fluid .offset1 { + margin-left: 10.638297872340425%; + *margin-left: 10.53191489361702%; +} + +.row-fluid .offset1:first-child { + margin-left: 8.51063829787234%; + *margin-left: 8.404255319148938%; +} + +[class*="span"].hide, +.row-fluid [class*="span"].hide { + display: none; +} + +[class*="span"].pull-right, +.row-fluid [class*="span"].pull-right { + float: right; +} + +.container { + margin-right: auto; + margin-left: auto; + *zoom: 1; +} + +.container:before, +.container:after { + display: table; + line-height: 0; + content: ""; +} + +.container:after { + clear: both; +} + +.container-fluid { + padding-right: 20px; + padding-left: 20px; + *zoom: 1; +} + +.container-fluid:before, +.container-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.container-fluid:after { + clear: both; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 21px; + font-weight: 200; + line-height: 30px; +} + +small { + font-size: 85%; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +cite { + font-style: normal; +} + +.muted { + color: #999999; +} + +a.muted:hover, +a.muted:focus { + color: #808080; +} + +.text-warning { + color: #c09853; +} + +a.text-warning:hover, +a.text-warning:focus { + color: #a47e3c; +} + +.text-error { + color: #b94a48; +} + +a.text-error:hover, +a.text-error:focus { + color: #953b39; +} + +.text-info { + color: #3a87ad; +} + +a.text-info:hover, +a.text-info:focus { + color: #2d6987; +} + +.text-success { + color: #468847; +} + +a.text-success:hover, +a.text-success:focus { + color: #356635; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 10px 0; + font-family: inherit; + font-weight: bold; + line-height: 20px; + color: inherit; + text-rendering: optimizelegibility; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1, +h2, +h3 { + line-height: 40px; +} + +h1 { + font-size: 38.5px; +} + +h2 { + font-size: 31.5px; +} + +h3 { + font-size: 24.5px; +} + +h4 { + font-size: 17.5px; +} + +h5 { + font-size: 14px; +} + +h6 { + font-size: 11.9px; +} + +h1 small { + font-size: 24.5px; +} + +h2 small { + font-size: 17.5px; +} + +h3 small { + font-size: 14px; +} + +h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 20px 0 30px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + padding: 0; + margin: 0 0 10px 25px; +} + +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} + +li { + line-height: 20px; +} + +ul.unstyled, +ol.unstyled { + margin-left: 0; + list-style: none; +} + +ul.inline, +ol.inline { + margin-left: 0; + list-style: none; +} + +ul.inline > li, +ol.inline > li { + display: inline-block; + *display: inline; + padding-right: 5px; + padding-left: 5px; + *zoom: 1; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 20px; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 10px; +} + +.dl-horizontal { + *zoom: 1; +} + +.dl-horizontal:before, +.dl-horizontal:after { + display: table; + line-height: 0; + content: ""; +} + +.dl-horizontal:after { + clear: both; +} + +.dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dl-horizontal dd { + margin-left: 180px; +} + +hr { + margin: 20px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + margin-bottom: 0; + font-size: 17.5px; + font-weight: 300; + line-height: 1.25; +} + +blockquote small { + display: block; + line-height: 20px; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + float: right; + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 20px; +} + +code, +pre { + padding: 0 3px 2px; + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +code { + padding: 2px 4px; + color: #d14; + white-space: nowrap; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 20px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + color: inherit; + white-space: pre; + white-space: pre-wrap; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +form { + margin: 0 0 20px; +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: 40px; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +legend small { + font-size: 15px; + color: #999999; +} + +label, +input, +button, +select, +textarea { + font-size: 14px; + font-weight: normal; + line-height: 20px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +label { + display: block; + margin-bottom: 5px; +} + +select, +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + display: inline-block; + height: 20px; + padding: 4px 6px; + margin-bottom: 10px; + font-size: 14px; + line-height: 20px; + color: #555555; + vertical-align: middle; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +input, +textarea, +.uneditable-input { + width: 206px; +} + +textarea { + height: auto; +} + +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + background-color: #ffffff; + border: 1px solid #cccccc; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus, +.uneditable-input:focus { + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + *margin-top: 0; + line-height: normal; +} + +input[type="file"], +input[type="image"], +input[type="submit"], +input[type="reset"], +input[type="button"], +input[type="radio"], +input[type="checkbox"] { + width: auto; +} + +select, +input[type="file"] { + height: 30px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 30px; +} + +select { + width: 220px; + background-color: #ffffff; + border: 1px solid #cccccc; +} + +select[multiple], +select[size] { + height: auto; +} + +select:focus, +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.uneditable-input, +.uneditable-textarea { + color: #999999; + cursor: not-allowed; + background-color: #fcfcfc; + border-color: #cccccc; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); +} + +.uneditable-input { + overflow: hidden; + white-space: nowrap; +} + +.uneditable-textarea { + width: auto; + height: auto; +} + +input:-moz-placeholder, +textarea:-moz-placeholder { + color: #999999; +} + +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #999999; +} + +input::-webkit-input-placeholder, +textarea::-webkit-input-placeholder { + color: #999999; +} + +.radio, +.checkbox { + min-height: 20px; + padding-left: 20px; +} + +.radio input[type="radio"], +.checkbox input[type="checkbox"] { + float: left; + margin-left: -20px; +} + +.controls > .radio:first-child, +.controls > .checkbox:first-child { + padding-top: 5px; +} + +.radio.inline, +.checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} + +.radio.inline + .radio.inline, +.checkbox.inline + .checkbox.inline { + margin-left: 10px; +} + +.input-mini { + width: 60px; +} + +.input-small { + width: 90px; +} + +.input-medium { + width: 150px; +} + +.input-large { + width: 210px; +} + +.input-xlarge { + width: 270px; +} + +.input-xxlarge { + width: 530px; +} + +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"] { + float: none; + margin-left: 0; +} + +.input-append input[class*="span"], +.input-append .uneditable-input[class*="span"], +.input-prepend input[class*="span"], +.input-prepend .uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"], +.row-fluid .input-prepend [class*="span"], +.row-fluid .input-append [class*="span"] { + display: inline-block; +} + +input, +textarea, +.uneditable-input { + margin-left: 0; +} + +.controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; +} + +input.span12, +textarea.span12, +.uneditable-input.span12 { + width: 926px; +} + +input.span11, +textarea.span11, +.uneditable-input.span11 { + width: 846px; +} + +input.span10, +textarea.span10, +.uneditable-input.span10 { + width: 766px; +} + +input.span9, +textarea.span9, +.uneditable-input.span9 { + width: 686px; +} + +input.span8, +textarea.span8, +.uneditable-input.span8 { + width: 606px; +} + +input.span7, +textarea.span7, +.uneditable-input.span7 { + width: 526px; +} + +input.span6, +textarea.span6, +.uneditable-input.span6 { + width: 446px; +} + +input.span5, +textarea.span5, +.uneditable-input.span5 { + width: 366px; +} + +input.span4, +textarea.span4, +.uneditable-input.span4 { + width: 286px; +} + +input.span3, +textarea.span3, +.uneditable-input.span3 { + width: 206px; +} + +input.span2, +textarea.span2, +.uneditable-input.span2 { + width: 126px; +} + +input.span1, +textarea.span1, +.uneditable-input.span1 { + width: 46px; +} + +.controls-row { + *zoom: 1; +} + +.controls-row:before, +.controls-row:after { + display: table; + line-height: 0; + content: ""; +} + +.controls-row:after { + clear: both; +} + +.controls-row [class*="span"], +.row-fluid .controls-row [class*="span"] { + float: left; +} + +.controls-row .checkbox[class*="span"], +.controls-row .radio[class*="span"] { + padding-top: 5px; +} + +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #eeeeee; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { + background-color: transparent; +} + +.control-group.warning .control-label, +.control-group.warning .help-block, +.control-group.warning .help-inline { + color: #c09853; +} + +.control-group.warning .checkbox, +.control-group.warning .radio, +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + color: #c09853; +} + +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.warning input:focus, +.control-group.warning select:focus, +.control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.control-group.warning .input-prepend .add-on, +.control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.control-group.error .control-label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: #b94a48; +} + +.control-group.error .checkbox, +.control-group.error .radio, +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + color: #b94a48; +} + +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.error input:focus, +.control-group.error select:focus, +.control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.control-group.error .input-prepend .add-on, +.control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.control-group.success .control-label, +.control-group.success .help-block, +.control-group.success .help-inline { + color: #468847; +} + +.control-group.success .checkbox, +.control-group.success .radio, +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + color: #468847; +} + +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.success input:focus, +.control-group.success select:focus, +.control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.control-group.success .input-prepend .add-on, +.control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +.control-group.info .control-label, +.control-group.info .help-block, +.control-group.info .help-inline { + color: #3a87ad; +} + +.control-group.info .checkbox, +.control-group.info .radio, +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + color: #3a87ad; +} + +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + border-color: #3a87ad; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.info input:focus, +.control-group.info select:focus, +.control-group.info textarea:focus { + border-color: #2d6987; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; +} + +.control-group.info .input-prepend .add-on, +.control-group.info .input-append .add-on { + color: #3a87ad; + background-color: #d9edf7; + border-color: #3a87ad; +} + +input:focus:invalid, +textarea:focus:invalid, +select:focus:invalid { + color: #b94a48; + border-color: #ee5f5b; +} + +input:focus:invalid:focus, +textarea:focus:invalid:focus, +select:focus:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} + +.form-actions { + padding: 19px 20px 20px; + margin-top: 20px; + margin-bottom: 20px; + background-color: #f5f5f5; + border-top: 1px solid #e5e5e5; + *zoom: 1; +} + +.form-actions:before, +.form-actions:after { + display: table; + line-height: 0; + content: ""; +} + +.form-actions:after { + clear: both; +} + +.help-block, +.help-inline { + color: #595959; +} + +.help-block { + display: block; + margin-bottom: 10px; +} + +.help-inline { + display: inline-block; + *display: inline; + padding-left: 5px; + vertical-align: middle; + *zoom: 1; +} + +.input-append, +.input-prepend { + display: inline-block; + margin-bottom: 10px; + font-size: 0; + white-space: nowrap; + vertical-align: middle; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input, +.input-append .dropdown-menu, +.input-prepend .dropdown-menu, +.input-append .popover, +.input-prepend .popover { + font-size: 14px; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input { + position: relative; + margin-bottom: 0; + *margin-left: 0; + vertical-align: top; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append input:focus, +.input-prepend input:focus, +.input-append select:focus, +.input-prepend select:focus, +.input-append .uneditable-input:focus, +.input-prepend .uneditable-input:focus { + z-index: 2; +} + +.input-append .add-on, +.input-prepend .add-on { + display: inline-block; + width: auto; + height: 20px; + min-width: 16px; + padding: 4px 5px; + font-size: 14px; + font-weight: normal; + line-height: 20px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #eeeeee; + border: 1px solid #ccc; +} + +.input-append .add-on, +.input-prepend .add-on, +.input-append .btn, +.input-prepend .btn, +.input-append .btn-group > .dropdown-toggle, +.input-prepend .btn-group > .dropdown-toggle { + vertical-align: top; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-append .active, +.input-prepend .active { + background-color: #a9dba9; + border-color: #46a546; +} + +.input-prepend .add-on, +.input-prepend .btn { + margin-right: -1px; +} + +.input-prepend .add-on:first-child, +.input-prepend .btn:first-child { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input, +.input-append select, +.input-append .uneditable-input { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input + .btn-group .btn:last-child, +.input-append select + .btn-group .btn:last-child, +.input-append .uneditable-input + .btn-group .btn:last-child { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append .add-on, +.input-append .btn, +.input-append .btn-group { + margin-left: -1px; +} + +.input-append .add-on:last-child, +.input-append .btn:last-child, +.input-append .btn-group:last-child > .dropdown-toggle { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append input, +.input-prepend.input-append select, +.input-prepend.input-append .uneditable-input { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-prepend.input-append input + .btn-group .btn, +.input-prepend.input-append select + .btn-group .btn, +.input-prepend.input-append .uneditable-input + .btn-group .btn { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .add-on:first-child, +.input-prepend.input-append .btn:first-child { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-prepend.input-append .add-on:last-child, +.input-prepend.input-append .btn:last-child { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .btn-group:first-child { + margin-left: 0; +} + +input.search-query { + padding-right: 14px; + padding-right: 4px \9; + padding-left: 14px; + padding-left: 4px \9; + /* IE7-8 doesn't have border-radius, so don't indent the padding */ + + margin-bottom: 0; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +/* Allow for input prepend/append in search forms */ + +.form-search .input-append .search-query, +.form-search .input-prepend .search-query { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.form-search .input-append .search-query { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search .input-append .btn { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .search-query { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .btn { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input, +.form-search .input-prepend, +.form-inline .input-prepend, +.form-horizontal .input-prepend, +.form-search .input-append, +.form-inline .input-append, +.form-horizontal .input-append { + display: inline-block; + *display: inline; + margin-bottom: 0; + vertical-align: middle; + *zoom: 1; +} + +.form-search .hide, +.form-inline .hide, +.form-horizontal .hide { + display: none; +} + +.form-search label, +.form-inline label, +.form-search .btn-group, +.form-inline .btn-group { + display: inline-block; +} + +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + margin-bottom: 0; +} + +.form-search .radio, +.form-search .checkbox, +.form-inline .radio, +.form-inline .checkbox { + padding-left: 0; + margin-bottom: 0; + vertical-align: middle; +} + +.form-search .radio input[type="radio"], +.form-search .checkbox input[type="checkbox"], +.form-inline .radio input[type="radio"], +.form-inline .checkbox input[type="checkbox"] { + float: left; + margin-right: 3px; + margin-left: 0; +} + +.control-group { + margin-bottom: 10px; +} + +legend + .control-group { + margin-top: 20px; + -webkit-margin-top-collapse: separate; +} + +.form-horizontal .control-group { + margin-bottom: 20px; + *zoom: 1; +} + +.form-horizontal .control-group:before, +.form-horizontal .control-group:after { + display: table; + line-height: 0; + content: ""; +} + +.form-horizontal .control-group:after { + clear: both; +} + +.form-horizontal .control-label { + float: left; + width: 160px; + padding-top: 5px; + text-align: right; +} + +.form-horizontal .controls { + *display: inline-block; + *padding-left: 20px; + margin-left: 180px; + *margin-left: 0; +} + +.form-horizontal .controls:first-child { + *padding-left: 180px; +} + +.form-horizontal .help-block { + margin-bottom: 0; +} + +.form-horizontal input + .help-block, +.form-horizontal select + .help-block, +.form-horizontal textarea + .help-block, +.form-horizontal .uneditable-input + .help-block, +.form-horizontal .input-prepend + .help-block, +.form-horizontal .input-append + .help-block { + margin-top: 10px; +} + +.form-horizontal .form-actions { + padding-left: 180px; +} + +table { + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table th, +.table td { + padding: 8px; + line-height: 20px; + text-align: left; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table th { + font-weight: bold; +} + +.table thead th { + vertical-align: bottom; +} + +.table caption + thead tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child th, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child th, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table .table { + background-color: #ffffff; +} + +.table-condensed th, +.table-condensed td { + padding: 4px 5px; +} + +.table-bordered { + border: 1px solid #dddddd; + border-collapse: separate; + *border-collapse: collapse; + border-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.table-bordered th, +.table-bordered td { + border-left: 1px solid #dddddd; +} + +.table-bordered caption + thead tr:first-child th, +.table-bordered caption + tbody tr:first-child th, +.table-bordered caption + tbody tr:first-child td, +.table-bordered colgroup + thead tr:first-child th, +.table-bordered colgroup + tbody tr:first-child th, +.table-bordered colgroup + tbody tr:first-child td, +.table-bordered thead:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} + +.table-bordered thead:first-child tr:first-child > th:first-child, +.table-bordered tbody:first-child tr:first-child > td:first-child, +.table-bordered tbody:first-child tr:first-child > th:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered thead:first-child tr:first-child > th:last-child, +.table-bordered tbody:first-child tr:first-child > td:last-child, +.table-bordered tbody:first-child tr:first-child > th:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:first-child, +.table-bordered tbody:last-child tr:last-child > td:first-child, +.table-bordered tbody:last-child tr:last-child > th:first-child, +.table-bordered tfoot:last-child tr:last-child > td:first-child, +.table-bordered tfoot:last-child tr:last-child > th:first-child { + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:last-child, +.table-bordered tbody:last-child tr:last-child > td:last-child, +.table-bordered tbody:last-child tr:last-child > th:last-child, +.table-bordered tfoot:last-child tr:last-child > td:last-child, +.table-bordered tfoot:last-child tr:last-child > th:last-child { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:first-child { + -webkit-border-bottom-left-radius: 0; + border-bottom-left-radius: 0; + -moz-border-radius-bottomleft: 0; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:last-child { + -webkit-border-bottom-right-radius: 0; + border-bottom-right-radius: 0; + -moz-border-radius-bottomright: 0; +} + +.table-bordered caption + thead tr:first-child th:first-child, +.table-bordered caption + tbody tr:first-child td:first-child, +.table-bordered colgroup + thead tr:first-child th:first-child, +.table-bordered colgroup + tbody tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered caption + thead tr:first-child th:last-child, +.table-bordered caption + tbody tr:first-child td:last-child, +.table-bordered colgroup + thead tr:first-child th:last-child, +.table-bordered colgroup + tbody tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-striped tbody > tr:nth-child(odd) > td, +.table-striped tbody > tr:nth-child(odd) > th { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover > td, +.table-hover tbody tr:hover > th { + background-color: #f5f5f5; +} + +table td[class*="span"], +table th[class*="span"], +.row-fluid table td[class*="span"], +.row-fluid table th[class*="span"] { + display: table-cell; + float: none; + margin-left: 0; +} + +.table td.span1, +.table th.span1 { + float: none; + width: 44px; + margin-left: 0; +} + +.table td.span2, +.table th.span2 { + float: none; + width: 124px; + margin-left: 0; +} + +.table td.span3, +.table th.span3 { + float: none; + width: 204px; + margin-left: 0; +} + +.table td.span4, +.table th.span4 { + float: none; + width: 284px; + margin-left: 0; +} + +.table td.span5, +.table th.span5 { + float: none; + width: 364px; + margin-left: 0; +} + +.table td.span6, +.table th.span6 { + float: none; + width: 444px; + margin-left: 0; +} + +.table td.span7, +.table th.span7 { + float: none; + width: 524px; + margin-left: 0; +} + +.table td.span8, +.table th.span8 { + float: none; + width: 604px; + margin-left: 0; +} + +.table td.span9, +.table th.span9 { + float: none; + width: 684px; + margin-left: 0; +} + +.table td.span10, +.table th.span10 { + float: none; + width: 764px; + margin-left: 0; +} + +.table td.span11, +.table th.span11 { + float: none; + width: 844px; + margin-left: 0; +} + +.table td.span12, +.table th.span12 { + float: none; + width: 924px; + margin-left: 0; +} + +.table tbody tr.success > td { + background-color: #dff0d8; +} + +.table tbody tr.error > td { + background-color: #f2dede; +} + +.table tbody tr.warning > td { + background-color: #fcf8e3; +} + +.table tbody tr.info > td { + background-color: #d9edf7; +} + +.table-hover tbody tr.success:hover > td { + background-color: #d0e9c6; +} + +.table-hover tbody tr.error:hover > td { + background-color: #ebcccc; +} + +.table-hover tbody tr.warning:hover > td { + background-color: #faf2cc; +} + +.table-hover tbody tr.info:hover > td { + background-color: #c4e3f3; +} + +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + margin-top: 1px; + *margin-right: .3em; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; +} + +/* White icons with optional class, or on hover/focus/active states of certain elements */ + +.icon-white, +.nav-pills > .active > a > [class^="icon-"], +.nav-pills > .active > a > [class*=" icon-"], +.nav-list > .active > a > [class^="icon-"], +.nav-list > .active > a > [class*=" icon-"], +.navbar-inverse .nav > .active > a > [class^="icon-"], +.navbar-inverse .nav > .active > a > [class*=" icon-"], +.dropdown-menu > li > a:hover > [class^="icon-"], +.dropdown-menu > li > a:focus > [class^="icon-"], +.dropdown-menu > li > a:hover > [class*=" icon-"], +.dropdown-menu > li > a:focus > [class*=" icon-"], +.dropdown-menu > .active > a > [class^="icon-"], +.dropdown-menu > .active > a > [class*=" icon-"], +.dropdown-submenu:hover > a > [class^="icon-"], +.dropdown-submenu:focus > a > [class^="icon-"], +.dropdown-submenu:hover > a > [class*=" icon-"], +.dropdown-submenu:focus > a > [class*=" icon-"] { + background-image: url("../img/glyphicons-halflings-white.png"); +} + +.icon-glass { + background-position: 0 0; +} + +.icon-music { + background-position: -24px 0; +} + +.icon-search { + background-position: -48px 0; +} + +.icon-envelope { + background-position: -72px 0; +} + +.icon-heart { + background-position: -96px 0; +} + +.icon-star { + background-position: -120px 0; +} + +.icon-star-empty { + background-position: -144px 0; +} + +.icon-user { + background-position: -168px 0; +} + +.icon-film { + background-position: -192px 0; +} + +.icon-th-large { + background-position: -216px 0; +} + +.icon-th { + background-position: -240px 0; +} + +.icon-th-list { + background-position: -264px 0; +} + +.icon-ok { + background-position: -288px 0; +} + +.icon-remove { + background-position: -312px 0; +} + +.icon-zoom-in { + background-position: -336px 0; +} + +.icon-zoom-out { + background-position: -360px 0; +} + +.icon-off { + background-position: -384px 0; +} + +.icon-signal { + background-position: -408px 0; +} + +.icon-cog { + background-position: -432px 0; +} + +.icon-trash { + background-position: -456px 0; +} + +.icon-home { + background-position: 0 -24px; +} + +.icon-file { + background-position: -24px -24px; +} + +.icon-time { + background-position: -48px -24px; +} + +.icon-road { + background-position: -72px -24px; +} + +.icon-download-alt { + background-position: -96px -24px; +} + +.icon-download { + background-position: -120px -24px; +} + +.icon-upload { + background-position: -144px -24px; +} + +.icon-inbox { + background-position: -168px -24px; +} + +.icon-play-circle { + background-position: -192px -24px; +} + +.icon-repeat { + background-position: -216px -24px; +} + +.icon-refresh { + background-position: -240px -24px; +} + +.icon-list-alt { + background-position: -264px -24px; +} + +.icon-lock { + background-position: -287px -24px; +} + +.icon-flag { + background-position: -312px -24px; +} + +.icon-headphones { + background-position: -336px -24px; +} + +.icon-volume-off { + background-position: -360px -24px; +} + +.icon-volume-down { + background-position: -384px -24px; +} + +.icon-volume-up { + background-position: -408px -24px; +} + +.icon-qrcode { + background-position: -432px -24px; +} + +.icon-barcode { + background-position: -456px -24px; +} + +.icon-tag { + background-position: 0 -48px; +} + +.icon-tags { + background-position: -25px -48px; +} + +.icon-book { + background-position: -48px -48px; +} + +.icon-bookmark { + background-position: -72px -48px; +} + +.icon-print { + background-position: -96px -48px; +} + +.icon-camera { + background-position: -120px -48px; +} + +.icon-font { + background-position: -144px -48px; +} + +.icon-bold { + background-position: -167px -48px; +} + +.icon-italic { + background-position: -192px -48px; +} + +.icon-text-height { + background-position: -216px -48px; +} + +.icon-text-width { + background-position: -240px -48px; +} + +.icon-align-left { + background-position: -264px -48px; +} + +.icon-align-center { + background-position: -288px -48px; +} + +.icon-align-right { + background-position: -312px -48px; +} + +.icon-align-justify { + background-position: -336px -48px; +} + +.icon-list { + background-position: -360px -48px; +} + +.icon-indent-left { + background-position: -384px -48px; +} + +.icon-indent-right { + background-position: -408px -48px; +} + +.icon-facetime-video { + background-position: -432px -48px; +} + +.icon-picture { + background-position: -456px -48px; +} + +.icon-pencil { + background-position: 0 -72px; +} + +.icon-map-marker { + background-position: -24px -72px; +} + +.icon-adjust { + background-position: -48px -72px; +} + +.icon-tint { + background-position: -72px -72px; +} + +.icon-edit { + background-position: -96px -72px; +} + +.icon-share { + background-position: -120px -72px; +} + +.icon-check { + background-position: -144px -72px; +} + +.icon-move { + background-position: -168px -72px; +} + +.icon-step-backward { + background-position: -192px -72px; +} + +.icon-fast-backward { + background-position: -216px -72px; +} + +.icon-backward { + background-position: -240px -72px; +} + +.icon-play { + background-position: -264px -72px; +} + +.icon-pause { + background-position: -288px -72px; +} + +.icon-stop { + background-position: -312px -72px; +} + +.icon-forward { + background-position: -336px -72px; +} + +.icon-fast-forward { + background-position: -360px -72px; +} + +.icon-step-forward { + background-position: -384px -72px; +} + +.icon-eject { + background-position: -408px -72px; +} + +.icon-chevron-left { + background-position: -432px -72px; +} + +.icon-chevron-right { + background-position: -456px -72px; +} + +.icon-plus-sign { + background-position: 0 -96px; +} + +.icon-minus-sign { + background-position: -24px -96px; +} + +.icon-remove-sign { + background-position: -48px -96px; +} + +.icon-ok-sign { + background-position: -72px -96px; +} + +.icon-question-sign { + background-position: -96px -96px; +} + +.icon-info-sign { + background-position: -120px -96px; +} + +.icon-screenshot { + background-position: -144px -96px; +} + +.icon-remove-circle { + background-position: -168px -96px; +} + +.icon-ok-circle { + background-position: -192px -96px; +} + +.icon-ban-circle { + background-position: -216px -96px; +} + +.icon-arrow-left { + background-position: -240px -96px; +} + +.icon-arrow-right { + background-position: -264px -96px; +} + +.icon-arrow-up { + background-position: -289px -96px; +} + +.icon-arrow-down { + background-position: -312px -96px; +} + +.icon-share-alt { + background-position: -336px -96px; +} + +.icon-resize-full { + background-position: -360px -96px; +} + +.icon-resize-small { + background-position: -384px -96px; +} + +.icon-plus { + background-position: -408px -96px; +} + +.icon-minus { + background-position: -433px -96px; +} + +.icon-asterisk { + background-position: -456px -96px; +} + +.icon-exclamation-sign { + background-position: 0 -120px; +} + +.icon-gift { + background-position: -24px -120px; +} + +.icon-leaf { + background-position: -48px -120px; +} + +.icon-fire { + background-position: -72px -120px; +} + +.icon-eye-open { + background-position: -96px -120px; +} + +.icon-eye-close { + background-position: -120px -120px; +} + +.icon-warning-sign { + background-position: -144px -120px; +} + +.icon-plane { + background-position: -168px -120px; +} + +.icon-calendar { + background-position: -192px -120px; +} + +.icon-random { + width: 16px; + background-position: -216px -120px; +} + +.icon-comment { + background-position: -240px -120px; +} + +.icon-magnet { + background-position: -264px -120px; +} + +.icon-chevron-up { + background-position: -288px -120px; +} + +.icon-chevron-down { + background-position: -313px -119px; +} + +.icon-retweet { + background-position: -336px -120px; +} + +.icon-shopping-cart { + background-position: -360px -120px; +} + +.icon-folder-close { + width: 16px; + background-position: -384px -120px; +} + +.icon-folder-open { + width: 16px; + background-position: -408px -120px; +} + +.icon-resize-vertical { + background-position: -432px -119px; +} + +.icon-resize-horizontal { + background-position: -456px -118px; +} + +.icon-hdd { + background-position: 0 -144px; +} + +.icon-bullhorn { + background-position: -24px -144px; +} + +.icon-bell { + background-position: -48px -144px; +} + +.icon-certificate { + background-position: -72px -144px; +} + +.icon-thumbs-up { + background-position: -96px -144px; +} + +.icon-thumbs-down { + background-position: -120px -144px; +} + +.icon-hand-right { + background-position: -144px -144px; +} + +.icon-hand-left { + background-position: -168px -144px; +} + +.icon-hand-up { + background-position: -192px -144px; +} + +.icon-hand-down { + background-position: -216px -144px; +} + +.icon-circle-arrow-right { + background-position: -240px -144px; +} + +.icon-circle-arrow-left { + background-position: -264px -144px; +} + +.icon-circle-arrow-up { + background-position: -288px -144px; +} + +.icon-circle-arrow-down { + background-position: -312px -144px; +} + +.icon-globe { + background-position: -336px -144px; +} + +.icon-wrench { + background-position: -360px -144px; +} + +.icon-tasks { + background-position: -384px -144px; +} + +.icon-filter { + background-position: -408px -144px; +} + +.icon-briefcase { + background-position: -432px -144px; +} + +.icon-fullscreen { + background-position: -456px -144px; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle { + *margin-bottom: -3px; +} + +.dropdown-toggle:active, +.open .dropdown-toggle { + outline: 0; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus, +.dropdown-submenu:hover > a, +.dropdown-submenu:focus > a { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + outline: 0; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #999999; +} + +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: default; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.open { + *z-index: 1000; +} + +.open > .dropdown-menu { + display: block; +} + +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid #000000; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +.dropdown-submenu { + position: relative; +} + +.dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + -webkit-border-radius: 0 6px 6px 6px; + -moz-border-radius: 0 6px 6px 6px; + border-radius: 0 6px 6px 6px; +} + +.dropdown-submenu:hover > .dropdown-menu { + display: block; +} + +.dropup .dropdown-submenu > .dropdown-menu { + top: auto; + bottom: 0; + margin-top: 0; + margin-bottom: -2px; + -webkit-border-radius: 5px 5px 5px 0; + -moz-border-radius: 5px 5px 5px 0; + border-radius: 5px 5px 5px 0; +} + +.dropdown-submenu > a:after { + display: block; + float: right; + width: 0; + height: 0; + margin-top: 5px; + margin-right: -10px; + border-color: transparent; + border-left-color: #cccccc; + border-style: solid; + border-width: 5px 0 5px 5px; + content: " "; +} + +.dropdown-submenu:hover > a:after { + border-left-color: #ffffff; +} + +.dropdown-submenu.pull-left { + float: none; +} + +.dropdown-submenu.pull-left > .dropdown-menu { + left: -100%; + margin-left: 10px; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.dropdown .dropdown-menu .nav-header { + padding-right: 20px; + padding-left: 20px; +} + +.typeahead { + z-index: 1051; + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-large { + padding: 24px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.well-small { + padding: 9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +.collapse.in { + height: auto; +} + +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 20px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover, +.close:focus { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; + filter: alpha(opacity=40); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.btn { + display: inline-block; + *display: inline; + padding: 4px 12px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 14px; + line-height: 20px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + cursor: pointer; + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border: 1px solid #cccccc; + *border: 0; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-bottom-color: #b3b3b3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:focus, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + color: #333333; + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover, +.btn:focus { + color: #333333; + text-decoration: none; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn.disabled, +.btn[disabled] { + cursor: default; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-large { + padding: 11px 19px; + font-size: 17.5px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.btn-large [class^="icon-"], +.btn-large [class*=" icon-"] { + margin-top: 4px; +} + +.btn-small { + padding: 2px 10px; + font-size: 11.9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-small [class^="icon-"], +.btn-small [class*=" icon-"] { + margin-top: 0; +} + +.btn-mini [class^="icon-"], +.btn-mini [class*=" icon-"] { + margin-top: -1px; +} + +.btn-mini { + padding: 0 6px; + font-size: 10.5px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} + +.btn-primary { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #006dcc; + *background-color: #0044cc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(to bottom, #0088cc, #0044cc); + background-repeat: repeat-x; + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + color: #ffffff; + background-color: #0044cc; + *background-color: #003bb3; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #003399 \9; +} + +.btn-warning { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #faa732; + *background-color: #f89406; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-warning:hover, +.btn-warning:focus, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + color: #ffffff; + background-color: #f89406; + *background-color: #df8505; +} + +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} + +.btn-danger { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #da4f49; + *background-color: #bd362f; + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); + background-repeat: repeat-x; + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-danger:hover, +.btn-danger:focus, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + color: #ffffff; + background-color: #bd362f; + *background-color: #a9302a; +} + +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} + +.btn-success { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #5bb75b; + *background-color: #51a351; + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(to bottom, #62c462, #51a351); + background-repeat: repeat-x; + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-success:hover, +.btn-success:focus, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + color: #ffffff; + background-color: #51a351; + *background-color: #499249; +} + +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} + +.btn-info { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #49afcd; + *background-color: #2f96b4; + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); + background-repeat: repeat-x; + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-info:hover, +.btn-info:focus, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + color: #ffffff; + background-color: #2f96b4; + *background-color: #2a85a0; +} + +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} + +.btn-inverse { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #363636; + *background-color: #222222; + background-image: -moz-linear-gradient(top, #444444, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); + background-image: -webkit-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(top, #444444, #222222); + background-image: linear-gradient(to bottom, #444444, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-inverse:hover, +.btn-inverse:focus, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + color: #ffffff; + background-color: #222222; + *background-color: #151515; +} + +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} + +button.btn, +input[type="submit"].btn { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} + +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} + +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} + +.btn-link, +.btn-link:active, +.btn-link[disabled] { + background-color: transparent; + background-image: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-link { + color: #0088cc; + cursor: pointer; + border-color: transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-link:hover, +.btn-link:focus { + color: #005580; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover, +.btn-link[disabled]:focus { + color: #333333; + text-decoration: none; +} + +.btn-group { + position: relative; + display: inline-block; + *display: inline; + *margin-left: .3em; + font-size: 0; + white-space: nowrap; + vertical-align: middle; + *zoom: 1; +} + +.btn-group:first-child { + *margin-left: 0; +} + +.btn-group + .btn-group { + margin-left: 5px; +} + +.btn-toolbar { + margin-top: 10px; + margin-bottom: 10px; + font-size: 0; +} + +.btn-toolbar > .btn + .btn, +.btn-toolbar > .btn-group + .btn, +.btn-toolbar > .btn + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn { + position: relative; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group > .btn + .btn { + margin-left: -1px; +} + +.btn-group > .btn, +.btn-group > .dropdown-menu, +.btn-group > .popover { + font-size: 14px; +} + +.btn-group > .btn-mini { + font-size: 10.5px; +} + +.btn-group > .btn-small { + font-size: 11.9px; +} + +.btn-group > .btn-large { + font-size: 17.5px; +} + +.btn-group > .btn:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.btn-group > .btn.large:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.btn-group > .btn.large:last-child, +.btn-group > .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active { + z-index: 2; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + *padding-top: 5px; + padding-right: 8px; + *padding-bottom: 5px; + padding-left: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group > .btn-mini + .dropdown-toggle { + *padding-top: 2px; + padding-right: 5px; + *padding-bottom: 2px; + padding-left: 5px; +} + +.btn-group > .btn-small + .dropdown-toggle { + *padding-top: 5px; + *padding-bottom: 4px; +} + +.btn-group > .btn-large + .dropdown-toggle { + *padding-top: 7px; + padding-right: 12px; + *padding-bottom: 7px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group.open .btn.dropdown-toggle { + background-color: #e6e6e6; +} + +.btn-group.open .btn-primary.dropdown-toggle { + background-color: #0044cc; +} + +.btn-group.open .btn-warning.dropdown-toggle { + background-color: #f89406; +} + +.btn-group.open .btn-danger.dropdown-toggle { + background-color: #bd362f; +} + +.btn-group.open .btn-success.dropdown-toggle { + background-color: #51a351; +} + +.btn-group.open .btn-info.dropdown-toggle { + background-color: #2f96b4; +} + +.btn-group.open .btn-inverse.dropdown-toggle { + background-color: #222222; +} + +.btn .caret { + margin-top: 8px; + margin-left: 0; +} + +.btn-large .caret { + margin-top: 6px; +} + +.btn-large .caret { + border-top-width: 5px; + border-right-width: 5px; + border-left-width: 5px; +} + +.btn-mini .caret, +.btn-small .caret { + margin-top: 8px; +} + +.dropup .btn-large .caret { + border-bottom-width: 5px; +} + +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.btn-group-vertical { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-group-vertical > .btn { + display: block; + float: none; + max-width: 100%; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group-vertical > .btn + .btn { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:first-child { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.btn-group-vertical > .btn:last-child { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.btn-group-vertical > .btn-large:first-child { + -webkit-border-radius: 6px 6px 0 0; + -moz-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; +} + +.btn-group-vertical > .btn-large:last-child { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 20px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.alert, +.alert h4 { + color: #c09853; +} + +.alert h4 { + margin: 0; +} + +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 20px; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-success h4 { + color: #468847; +} + +.alert-danger, +.alert-error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-danger h4, +.alert-error h4 { + color: #b94a48; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-info h4 { + color: #3a87ad; +} + +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} + +.alert-block > p, +.alert-block > ul { + margin-bottom: 0; +} + +.alert-block p + p { + margin-top: 5px; +} + +.nav { + margin-bottom: 20px; + margin-left: 0; + list-style: none; +} + +.nav > li > a { + display: block; +} + +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > li > a > img { + max-width: none; +} + +.nav > .pull-right { + float: right; +} + +.nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.nav li + .nav-header { + margin-top: 9px; +} + +.nav-list { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 0; +} + +.nav-list > li > a, +.nav-list .nav-header { + margin-right: -15px; + margin-left: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} + +.nav-list > li > a { + padding: 3px 15px; +} + +.nav-list > .active > a, +.nav-list > .active > a:hover, +.nav-list > .active > a:focus { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} + +.nav-list [class^="icon-"], +.nav-list [class*=" icon-"] { + margin-right: 2px; +} + +.nav-list .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.nav-tabs, +.nav-pills { + *zoom: 1; +} + +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + line-height: 0; + content: ""; +} + +.nav-tabs:after, +.nav-pills:after { + clear: both; +} + +.nav-tabs > li, +.nav-pills > li { + float: left; +} + +.nav-tabs > li > a, +.nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs > li { + margin-bottom: -1px; +} + +.nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 20px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover, +.nav-tabs > li > a:focus { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover, +.nav-tabs > .active > a:focus { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} + +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.nav-pills > .active > a, +.nav-pills > .active > a:hover, +.nav-pills > .active > a:focus { + color: #ffffff; + background-color: #0088cc; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li > a { + margin-right: 0; +} + +.nav-tabs.nav-stacked { + border-bottom: 0; +} + +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-topleft: 4px; +} + +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.nav-tabs.nav-stacked > li > a:hover, +.nav-tabs.nav-stacked > li > a:focus { + z-index: 2; + border-color: #ddd; +} + +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} + +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} + +.nav-tabs .dropdown-menu { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.nav-pills .dropdown-menu { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.nav .dropdown-toggle .caret { + margin-top: 6px; + border-top-color: #0088cc; + border-bottom-color: #0088cc; +} + +.nav .dropdown-toggle:hover .caret, +.nav .dropdown-toggle:focus .caret { + border-top-color: #005580; + border-bottom-color: #005580; +} + +/* move down carets for tabs */ + +.nav-tabs .dropdown-toggle .caret { + margin-top: 8px; +} + +.nav .active .dropdown-toggle .caret { + border-top-color: #fff; + border-bottom-color: #fff; +} + +.nav-tabs .active .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.nav > .dropdown.active > a:hover, +.nav > .dropdown.active > a:focus { + cursor: pointer; +} + +.nav-tabs .open .dropdown-toggle, +.nav-pills .open .dropdown-toggle, +.nav > li.dropdown.open.active > a:hover, +.nav > li.dropdown.open.active > a:focus { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} + +.nav li.dropdown.open .caret, +.nav li.dropdown.open.active .caret, +.nav li.dropdown.open a:hover .caret, +.nav li.dropdown.open a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} + +.tabs-stacked .open > a:hover, +.tabs-stacked .open > a:focus { + border-color: #999999; +} + +.tabbable { + *zoom: 1; +} + +.tabbable:before, +.tabbable:after { + display: table; + line-height: 0; + content: ""; +} + +.tabbable:after { + clear: both; +} + +.tab-content { + overflow: auto; +} + +.tabs-below > .nav-tabs, +.tabs-right > .nav-tabs, +.tabs-left > .nav-tabs { + border-bottom: 0; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.tabs-below > .nav-tabs { + border-top: 1px solid #ddd; +} + +.tabs-below > .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} + +.tabs-below > .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.tabs-below > .nav-tabs > li > a:hover, +.tabs-below > .nav-tabs > li > a:focus { + border-top-color: #ddd; + border-bottom-color: transparent; +} + +.tabs-below > .nav-tabs > .active > a, +.tabs-below > .nav-tabs > .active > a:hover, +.tabs-below > .nav-tabs > .active > a:focus { + border-color: transparent #ddd #ddd #ddd; +} + +.tabs-left > .nav-tabs > li, +.tabs-right > .nav-tabs > li { + float: none; +} + +.tabs-left > .nav-tabs > li > a, +.tabs-right > .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} + +.tabs-left > .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} + +.tabs-left > .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.tabs-left > .nav-tabs > li > a:hover, +.tabs-left > .nav-tabs > li > a:focus { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} + +.tabs-left > .nav-tabs .active > a, +.tabs-left > .nav-tabs .active > a:hover, +.tabs-left > .nav-tabs .active > a:focus { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} + +.tabs-right > .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} + +.tabs-right > .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.tabs-right > .nav-tabs > li > a:hover, +.tabs-right > .nav-tabs > li > a:focus { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} + +.tabs-right > .nav-tabs .active > a, +.tabs-right > .nav-tabs .active > a:hover, +.tabs-right > .nav-tabs .active > a:focus { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} + +.nav > .disabled > a { + color: #999999; +} + +.nav > .disabled > a:hover, +.nav > .disabled > a:focus { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.navbar { + *position: relative; + *z-index: 2; + margin-bottom: 20px; + overflow: visible; +} + +.navbar-inner { + min-height: 40px; + padding-right: 20px; + padding-left: 20px; + background-color: #fafafa; + background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); + background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); + background-repeat: repeat-x; + border: 1px solid #d4d4d4; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); + *zoom: 1; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); +} + +.navbar-inner:before, +.navbar-inner:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-inner:after { + clear: both; +} + +.navbar .container { + width: auto; +} + +.nav-collapse.collapse { + height: auto; + overflow: visible; +} + +.navbar .brand { + display: block; + float: left; + padding: 10px 20px 10px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + color: #777777; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .brand:hover, +.navbar .brand:focus { + text-decoration: none; +} + +.navbar-text { + margin-bottom: 0; + line-height: 40px; + color: #777777; +} + +.navbar-link { + color: #777777; +} + +.navbar-link:hover, +.navbar-link:focus { + color: #333333; +} + +.navbar .divider-vertical { + height: 40px; + margin: 0 9px; + border-right: 1px solid #ffffff; + border-left: 1px solid #f2f2f2; +} + +.navbar .btn, +.navbar .btn-group { + margin-top: 5px; +} + +.navbar .btn-group .btn, +.navbar .input-prepend .btn, +.navbar .input-append .btn, +.navbar .input-prepend .btn-group, +.navbar .input-append .btn-group { + margin-top: 0; +} + +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} + +.navbar-form:before, +.navbar-form:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-form:after { + clear: both; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .radio, +.navbar-form .checkbox { + margin-top: 5px; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .btn { + display: inline-block; + margin-bottom: 0; +} + +.navbar-form input[type="image"], +.navbar-form input[type="checkbox"], +.navbar-form input[type="radio"] { + margin-top: 3px; +} + +.navbar-form .input-append, +.navbar-form .input-prepend { + margin-top: 5px; + white-space: nowrap; +} + +.navbar-form .input-append input, +.navbar-form .input-prepend input { + margin-top: 0; +} + +.navbar-search { + position: relative; + float: left; + margin-top: 5px; + margin-bottom: 0; +} + +.navbar-search .search-query { + padding: 4px 14px; + margin-bottom: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.navbar-static-top { + position: static; + margin-bottom: 0; +} + +.navbar-static-top .navbar-inner { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; + margin-bottom: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + border-width: 0 0 1px; +} + +.navbar-fixed-bottom .navbar-inner { + border-width: 1px 0 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner { + padding-right: 0; + padding-left: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar-fixed-bottom .navbar-inner { + -webkit-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} + +.navbar .nav.pull-right { + float: right; + margin-right: 0; +} + +.navbar .nav > li { + float: left; +} + +.navbar .nav > li > a { + float: none; + padding: 10px 15px 10px; + color: #777777; + text-decoration: none; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .nav .dropdown-toggle .caret { + margin-top: 8px; +} + +.navbar .nav > li > a:focus, +.navbar .nav > li > a:hover { + color: #333333; + text-decoration: none; + background-color: transparent; +} + +.navbar .nav > .active > a, +.navbar .nav > .active > a:hover, +.navbar .nav > .active > a:focus { + color: #555555; + text-decoration: none; + background-color: #e5e5e5; + -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); +} + +.navbar .btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-right: 5px; + margin-left: 5px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #ededed; + *background-color: #e5e5e5; + background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); + background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); + background-repeat: repeat-x; + border-color: #e5e5e5 #e5e5e5 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} + +.navbar .btn-navbar:hover, +.navbar .btn-navbar:focus, +.navbar .btn-navbar:active, +.navbar .btn-navbar.active, +.navbar .btn-navbar.disabled, +.navbar .btn-navbar[disabled] { + color: #ffffff; + background-color: #e5e5e5; + *background-color: #d9d9d9; +} + +.navbar .btn-navbar:active, +.navbar .btn-navbar.active { + background-color: #cccccc \9; +} + +.navbar .btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} + +.navbar .nav > li > .dropdown-menu:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.navbar .nav > li > .dropdown-menu:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:before { + top: auto; + bottom: -7px; + border-top: 7px solid #ccc; + border-bottom: 0; + border-top-color: rgba(0, 0, 0, 0.2); +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:after { + top: auto; + bottom: -6px; + border-top: 6px solid #ffffff; + border-bottom: 0; +} + +.navbar .nav li.dropdown > a:hover .caret, +.navbar .nav li.dropdown > a:focus .caret { + border-top-color: #333333; + border-bottom-color: #333333; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle, +.navbar .nav li.dropdown.active > .dropdown-toggle, +.navbar .nav li.dropdown.open.active > .dropdown-toggle { + color: #555555; + background-color: #e5e5e5; +} + +.navbar .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #777777; + border-bottom-color: #777777; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar .pull-right > li > .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:before, +.navbar .nav > li > .dropdown-menu.pull-right:before { + right: 12px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:after, +.navbar .nav > li > .dropdown-menu.pull-right:after { + right: 13px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { + right: 100%; + left: auto; + margin-right: -1px; + margin-left: 0; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.navbar-inverse .navbar-inner { + background-color: #1b1b1b; + background-image: -moz-linear-gradient(top, #222222, #111111); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); + background-image: -webkit-linear-gradient(top, #222222, #111111); + background-image: -o-linear-gradient(top, #222222, #111111); + background-image: linear-gradient(to bottom, #222222, #111111); + background-repeat: repeat-x; + border-color: #252525; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); +} + +.navbar-inverse .brand, +.navbar-inverse .nav > li > a { + color: #999999; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.navbar-inverse .brand:hover, +.navbar-inverse .nav > li > a:hover, +.navbar-inverse .brand:focus, +.navbar-inverse .nav > li > a:focus { + color: #ffffff; +} + +.navbar-inverse .brand { + color: #999999; +} + +.navbar-inverse .navbar-text { + color: #999999; +} + +.navbar-inverse .nav > li > a:focus, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .nav .active > a, +.navbar-inverse .nav .active > a:hover, +.navbar-inverse .nav .active > a:focus { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover, +.navbar-inverse .navbar-link:focus { + color: #ffffff; +} + +.navbar-inverse .divider-vertical { + border-right-color: #222222; + border-left-color: #111111; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .nav li.dropdown > a:hover .caret, +.navbar-inverse .nav li.dropdown > a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-search .search-query { + color: #ffffff; + background-color: #515151; + border-color: #111111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; +} + +.navbar-inverse .navbar-search .search-query:-moz-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:focus, +.navbar-inverse .navbar-search .search-query.focused { + padding: 5px 15px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + outline: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); +} + +.navbar-inverse .btn-navbar { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e0e0e; + *background-color: #040404; + background-image: -moz-linear-gradient(top, #151515, #040404); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); + background-image: -webkit-linear-gradient(top, #151515, #040404); + background-image: -o-linear-gradient(top, #151515, #040404); + background-image: linear-gradient(to bottom, #151515, #040404); + background-repeat: repeat-x; + border-color: #040404 #040404 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.navbar-inverse .btn-navbar:hover, +.navbar-inverse .btn-navbar:focus, +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active, +.navbar-inverse .btn-navbar.disabled, +.navbar-inverse .btn-navbar[disabled] { + color: #ffffff; + background-color: #040404; + *background-color: #000000; +} + +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active { + background-color: #000000 \9; +} + +.breadcrumb { + padding: 8px 15px; + margin: 0 0 20px; + list-style: none; + background-color: #f5f5f5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.breadcrumb > li { + display: inline-block; + *display: inline; + text-shadow: 0 1px 0 #ffffff; + *zoom: 1; +} + +.breadcrumb > li > .divider { + padding: 0 5px; + color: #ccc; +} + +.breadcrumb > .active { + color: #999999; +} + +.pagination { + margin: 20px 0; +} + +.pagination ul { + display: inline-block; + *display: inline; + margin-bottom: 0; + margin-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + *zoom: 1; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.pagination ul > li { + display: inline; +} + +.pagination ul > li > a, +.pagination ul > li > span { + float: left; + padding: 4px 12px; + line-height: 20px; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; + border-left-width: 0; +} + +.pagination ul > li > a:hover, +.pagination ul > li > a:focus, +.pagination ul > .active > a, +.pagination ul > .active > span { + background-color: #f5f5f5; +} + +.pagination ul > .active > a, +.pagination ul > .active > span { + color: #999999; + cursor: default; +} + +.pagination ul > .disabled > span, +.pagination ul > .disabled > a, +.pagination ul > .disabled > a:hover, +.pagination ul > .disabled > a:focus { + color: #999999; + cursor: default; + background-color: transparent; +} + +.pagination ul > li:first-child > a, +.pagination ul > li:first-child > span { + border-left-width: 1px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.pagination ul > li:last-child > a, +.pagination ul > li:last-child > span { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.pagination-centered { + text-align: center; +} + +.pagination-right { + text-align: right; +} + +.pagination-large ul > li > a, +.pagination-large ul > li > span { + padding: 11px 19px; + font-size: 17.5px; +} + +.pagination-large ul > li:first-child > a, +.pagination-large ul > li:first-child > span { + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.pagination-large ul > li:last-child > a, +.pagination-large ul > li:last-child > span { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.pagination-mini ul > li:first-child > a, +.pagination-small ul > li:first-child > a, +.pagination-mini ul > li:first-child > span, +.pagination-small ul > li:first-child > span { + -webkit-border-bottom-left-radius: 3px; + border-bottom-left-radius: 3px; + -webkit-border-top-left-radius: 3px; + border-top-left-radius: 3px; + -moz-border-radius-bottomleft: 3px; + -moz-border-radius-topleft: 3px; +} + +.pagination-mini ul > li:last-child > a, +.pagination-small ul > li:last-child > a, +.pagination-mini ul > li:last-child > span, +.pagination-small ul > li:last-child > span { + -webkit-border-top-right-radius: 3px; + border-top-right-radius: 3px; + -webkit-border-bottom-right-radius: 3px; + border-bottom-right-radius: 3px; + -moz-border-radius-topright: 3px; + -moz-border-radius-bottomright: 3px; +} + +.pagination-small ul > li > a, +.pagination-small ul > li > span { + padding: 2px 10px; + font-size: 11.9px; +} + +.pagination-mini ul > li > a, +.pagination-mini ul > li > span { + padding: 0 6px; + font-size: 10.5px; +} + +.pager { + margin: 20px 0; + text-align: center; + list-style: none; + *zoom: 1; +} + +.pager:before, +.pager:after { + display: table; + line-height: 0; + content: ""; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #f5f5f5; +} + +.pager .next > a, +.pager .next > span { + float: right; +} + +.pager .previous > a, +.pager .previous > span { + float: left; +} + +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #999999; + cursor: default; + background-color: #fff; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.modal { + position: fixed; + top: 10%; + left: 50%; + z-index: 1050; + width: 560px; + margin-left: -280px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + outline: none; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} + +.modal.fade { + top: -25%; + -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; + -moz-transition: opacity 0.3s linear, top 0.3s ease-out; + -o-transition: opacity 0.3s linear, top 0.3s ease-out; + transition: opacity 0.3s linear, top 0.3s ease-out; +} + +.modal.fade.in { + top: 10%; +} + +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} + +.modal-header .close { + margin-top: 2px; +} + +.modal-header h3 { + margin: 0; + line-height: 30px; +} + +.modal-body { + position: relative; + max-height: 400px; + padding: 15px; + overflow-y: auto; +} + +.modal-form { + margin-bottom: 0; +} + +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + line-height: 0; + content: ""; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + font-size: 11px; + line-height: 1.4; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} + +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} + +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} + +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + white-space: normal; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.popover.top { + margin-top: -10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-left: -10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.popover-title:empty { + display: none; +} + +.popover-content { + padding: 9px 14px; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow { + border-width: 11px; +} + +.popover .arrow:after { + border-width: 10px; + content: ""; +} + +.popover.top .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.top .arrow:after { + bottom: 1px; + margin-left: -10px; + border-top-color: #ffffff; + border-bottom-width: 0; +} + +.popover.right .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.right .arrow:after { + bottom: -10px; + left: 1px; + border-right-color: #ffffff; + border-left-width: 0; +} + +.popover.bottom .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-top-width: 0; +} + +.popover.bottom .arrow:after { + top: 1px; + margin-left: -10px; + border-bottom-color: #ffffff; + border-top-width: 0; +} + +.popover.left .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, 0.25); + border-right-width: 0; +} + +.popover.left .arrow:after { + right: 1px; + bottom: -10px; + border-left-color: #ffffff; + border-right-width: 0; +} + +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} + +.thumbnails:before, +.thumbnails:after { + display: table; + line-height: 0; + content: ""; +} + +.thumbnails:after { + clear: both; +} + +.row-fluid .thumbnails { + margin-left: 0; +} + +.thumbnails > li { + float: left; + margin-bottom: 20px; + margin-left: 20px; +} + +.thumbnail { + display: block; + padding: 4px; + line-height: 20px; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +a.thumbnail:hover, +a.thumbnail:focus { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} + +.thumbnail > img { + display: block; + max-width: 100%; + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #555555; +} + +.media, +.media-body { + overflow: hidden; + *overflow: visible; + zoom: 1; +} + +.media, +.media .media { + margin-top: 15px; +} + +.media:first-child { + margin-top: 0; +} + +.media-object { + display: block; +} + +.media-heading { + margin: 0 0 5px; +} + +.media > .pull-left { + margin-right: 10px; +} + +.media > .pull-right { + margin-left: 10px; +} + +.media-list { + margin-left: 0; + list-style: none; +} + +.label, +.badge { + display: inline-block; + padding: 2px 4px; + font-size: 11.844px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +.label { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.badge { + padding-right: 9px; + padding-left: 9px; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; +} + +.label:empty, +.badge:empty { + display: none; +} + +a.label:hover, +a.label:focus, +a.badge:hover, +a.badge:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label-important, +.badge-important { + background-color: #b94a48; +} + +.label-important[href], +.badge-important[href] { + background-color: #953b39; +} + +.label-warning, +.badge-warning { + background-color: #f89406; +} + +.label-warning[href], +.badge-warning[href] { + background-color: #c67605; +} + +.label-success, +.badge-success { + background-color: #468847; +} + +.label-success[href], +.badge-success[href] { + background-color: #356635; +} + +.label-info, +.badge-info { + background-color: #3a87ad; +} + +.label-info[href], +.badge-info[href] { + background-color: #2d6987; +} + +.label-inverse, +.badge-inverse { + background-color: #333333; +} + +.label-inverse[href], +.badge-inverse[href] { + background-color: #1a1a1a; +} + +.btn .label, +.btn .badge { + position: relative; + top: -1px; +} + +.btn-mini .label, +.btn-mini .badge { + top: 0; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-ms-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress .bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(to bottom, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress .bar + .bar { + -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); +} + +.progress-striped .bar { + background-color: #149bdf; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} + +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-danger .bar, +.progress .bar-danger { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); +} + +.progress-danger.progress-striped .bar, +.progress-striped .bar-danger { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-success .bar, +.progress .bar-success { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(to bottom, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); +} + +.progress-success.progress-striped .bar, +.progress-striped .bar-success { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-info .bar, +.progress .bar-info { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(to bottom, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); +} + +.progress-info.progress-striped .bar, +.progress-striped .bar-info { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-warning .bar, +.progress .bar-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); +} + +.progress-warning.progress-striped .bar, +.progress-striped .bar-warning { + background-color: #fbb450; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.accordion { + margin-bottom: 20px; +} + +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.accordion-heading { + border-bottom: 0; +} + +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} + +.accordion-toggle { + cursor: pointer; +} + +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} + +.carousel { + position: relative; + margin-bottom: 20px; + line-height: 1; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + line-height: 1; +} + +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.right { + right: 15px; + left: auto; +} + +.carousel-control:hover, +.carousel-control:focus { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-indicators { + position: absolute; + top: 15px; + right: 15px; + z-index: 5; + margin: 0; + list-style: none; +} + +.carousel-indicators li { + display: block; + float: left; + width: 10px; + height: 10px; + margin-left: 5px; + text-indent: -999px; + background-color: #ccc; + background-color: rgba(255, 255, 255, 0.25); + border-radius: 5px; +} + +.carousel-indicators .active { + background-color: #fff; +} + +.carousel-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 15px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} + +.carousel-caption h4, +.carousel-caption p { + line-height: 20px; + color: #ffffff; +} + +.carousel-caption h4 { + margin: 0 0 5px; +} + +.carousel-caption p { + margin-bottom: 0; +} + +.hero-unit { + padding: 60px; + margin-bottom: 30px; + font-size: 18px; + font-weight: 200; + line-height: 30px; + color: inherit; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; + color: inherit; +} + +.hero-unit li { + line-height: 30px; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.invisible { + visibility: hidden; +} + +.affix { + position: fixed; +} diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/css/style.css b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/css/style.css new file mode 100644 index 00000000000..d6765976594 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/css/style.css @@ -0,0 +1,48 @@ +/* +Copyright 2017 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. +*/ + +body { + padding-top: 40px; + padding-bottom: 40px; + background-color: ${symbol_pound}f5f5f5; +} + +blockquote { + margin-bottom: 10px; + border-left-color: ${symbol_pound}bbb; +} + +form { + margin-top: 10px; +} + +.form-signin input[type="text"] { + font-size: 16px; + height: auto; + margin-bottom: 15px; + padding: 7px 9px; +} + +.row { + margin-left: 0px; + margin-top: 10px; + overflow: scroll; +} + +.label { + width: 90px; + display: inline-block; +} diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/index.html b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/index.html new file mode 100644 index 00000000000..21b4491cfb8 --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/index.html @@ -0,0 +1,101 @@ + + + + + + Hello Endpoints! + + + + + + + +
+ +
+ +
+

Get Greeting

+
Greeting ID:
+
+
+ +
+

List Greetings

+
+
+ +
+

Multiply Greetings

+
Greeting:
+
Count:
+
+
+ +
+

Authenticated Greeting

+
+
+ + + +
+ + diff --git a/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/js/base.js b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/js/base.js new file mode 100644 index 00000000000..8b74d85653f --- /dev/null +++ b/appengine-java8/endpoints-frameworks-v2/migration-example/src/main/webapp/js/base.js @@ -0,0 +1,208 @@ +/* +Copyright 2017 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. +*/ + +/** + * @fileoverview + * Provides methods for the Hello Endpoints sample UI and interaction with the + * Hello Endpoints API. + * + * @author danielholevoet@google.com (Dan Holevoet) + */ + +/** google global namespace for Google projects. */ +var google = google || {}; + +/** devrel namespace for Google Developer Relations projects. */ +google.devrel = google.devrel || {}; + +/** samples namespace for DevRel sample code. */ +google.devrel.samples = google.devrel.samples || {}; + +/** hello namespace for this sample. */ +google.devrel.samples.hello = google.devrel.samples.hello || {}; + +/** + * Client ID of the application (from the APIs Console). + * @type {string} + */ +google.devrel.samples.hello.CLIENT_ID = + 'replace this with your web application client ID'; + +/** + * Scopes used by the application. + * @type {string} + */ +google.devrel.samples.hello.SCOPES = + 'https://www.googleapis.com/auth/userinfo.email'; + +/** + * Whether or not the user is signed in. + * @type {boolean} + */ +google.devrel.samples.hello.signedIn = false; + +/** + * Loads the application UI after the user has completed auth. + */ +google.devrel.samples.hello.userAuthed = function() { + var request = gapi.client.oauth2.userinfo.get().execute(function(resp) { + if (!resp.code) { + google.devrel.samples.hello.signedIn = true; + document.getElementById('signinButton').innerHTML = 'Sign out'; + document.getElementById('authedGreeting').disabled = false; + } + }); +}; + +/** + * Handles the auth flow, with the given value for immediate mode. + * @param {boolean} mode Whether or not to use immediate mode. + * @param {Function} callback Callback to call on completion. + */ +google.devrel.samples.hello.signin = function(mode, callback) { + gapi.auth.authorize({client_id: google.devrel.samples.hello.CLIENT_ID, + scope: google.devrel.samples.hello.SCOPES, immediate: mode}, + callback); +}; + +/** + * Presents the user with the authorization popup. + */ +google.devrel.samples.hello.auth = function() { + if (!google.devrel.samples.hello.signedIn) { + google.devrel.samples.hello.signin(false, + google.devrel.samples.hello.userAuthed); + } else { + google.devrel.samples.hello.signedIn = false; + document.getElementById('signinButton').innerHTML = 'Sign in'; + document.getElementById('authedGreeting').disabled = true; + } +}; + +/** + * Prints a greeting to the greeting log. + * param {Object} greeting Greeting to print. + */ +google.devrel.samples.hello.print = function(greeting) { + var element = document.createElement('div'); + element.classList.add('row'); + element.innerHTML = greeting.message; + document.getElementById('outputLog').appendChild(element); +}; + +/** + * Gets a numbered greeting via the API. + * @param {string} id ID of the greeting. + */ +google.devrel.samples.hello.getGreeting = function(id) { + gapi.client.helloworld.greetings.getGreeting({'id': id}).execute( + function(resp) { + if (!resp.code) { + google.devrel.samples.hello.print(resp); + } + }); +}; + +/** + * Lists greetings via the API. + */ +google.devrel.samples.hello.listGreeting = function() { + gapi.client.helloworld.greetings.listGreeting().execute( + function(resp) { + if (!resp.code) { + resp.items = resp.items || []; + for (var i = 0; i < resp.items.length; i++) { + google.devrel.samples.hello.print(resp.items[i]); + } + } + }); +}; + +/** + * Gets a greeting a specified number of times. + * @param {string} greeting Greeting to repeat. + * @param {string} count Number of times to repeat it. + */ +google.devrel.samples.hello.multiplyGreeting = function( + greeting, times) { + gapi.client.helloworld.greetings.multiply({ + 'message': greeting, + 'times': times + }).execute(function(resp) { + if (!resp.code) { + google.devrel.samples.hello.print(resp); + } + }); +}; + +/** + * Greets the current user via the API. + */ +google.devrel.samples.hello.authedGreeting = function(id) { + gapi.client.helloworld.greetings.authed().execute( + function(resp) { + google.devrel.samples.hello.print(resp); + }); +}; + +/** + * Enables the button callbacks in the UI. + */ +google.devrel.samples.hello.enableButtons = function() { + document.getElementById('getGreeting').onclick = function() { + google.devrel.samples.hello.getGreeting( + document.getElementById('id').value); + } + + document.getElementById('listGreeting').onclick = function() { + google.devrel.samples.hello.listGreeting(); + } + + document.getElementById('multiplyGreetings').onclick = function() { + google.devrel.samples.hello.multiplyGreeting( + document.getElementById('greeting').value, + document.getElementById('count').value); + } + + document.getElementById('authedGreeting').onclick = function() { + google.devrel.samples.hello.authedGreeting(); + } + + document.getElementById('signinButton').onclick = function() { + google.devrel.samples.hello.auth(); + } +}; + +/** + * Initializes the application. + * @param {string} apiRoot Root of the API's path. + */ +google.devrel.samples.hello.init = function(apiRoot) { + // Loads the OAuth and helloworld APIs asynchronously, and triggers login + // when they have completed. + var apisToLoad; + var callback = function() { + if (--apisToLoad == 0) { + google.devrel.samples.hello.enableButtons(); + google.devrel.samples.hello.signin(true, + google.devrel.samples.hello.userAuthed); + } + } + + apisToLoad = 2; // must match number of calls to gapi.client.load() + gapi.client.load('helloworld', 'v1', callback, apiRoot); + gapi.client.load('oauth2', 'v2', callback); +}; diff --git a/appengine-java8/firebase-event-proxy/README.md b/appengine-java8/firebase-event-proxy/README.md new file mode 100644 index 00000000000..1b8c0317a11 --- /dev/null +++ b/appengine-java8/firebase-event-proxy/README.md @@ -0,0 +1,49 @@ +# App Engine Firebase Event Proxy + +An example app that illustrates how to create a Java App Engine Standard Environment +app that proxies Firebase events to another App Engine app. + +# Java Firebase Event Proxy +Illustrates how to authenticate and subscribe to Firebase from Java App Engine. + +# Python App Engine Listener +Illustrates how to authenticate messages received from the proxy app. + +## Setup + +### Java Firebase Event Proxy +Firebase Secret +Put your Firebase secret in the file: +gae-firebase-event-proxy/src/main/webapp/firebase-secret.properties +``` +firebaseSecret= +``` + +* Billing must be enabled from Cloud console. +* Manual scaling should turned on and configured to 1 instance in appengine-web.xml + +## Running locally +### Java Firebase Event Proxy +``` +cd gae-firebase-event-proxy +mvn appengine:run +``` + +### Python App Engine Listener +``` +cd gae-firebase-listener-python +dev_appserver . +``` + +## Deploying + +### Java Firebase Event Proxy +``` +cd gae-firebase-event-proxy +mvn appengine:deploy +``` + +### Python App Engine Listener +``` +appcfg.py -A -V v1 update gae-firebase-listener-python +``` diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/pom.xml b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/pom.xml new file mode 100644 index 00000000000..374ce1fa4cf --- /dev/null +++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + + com.example.gaefirebaseeventproxy + gaefirebaseeventproxy-j8 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + ../.. + + + + gae-firebase-event-proxy + 1 + UTF-8 + true + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + jstl + jstl + 1.2 + + + com.google.firebase + firebase-server-sdk + 3.0.3 + + + com.fasterxml.jackson.core + jackson-core + 2.9.0.pr3 + + + com.fasterxml.jackson.core + jackson-databind + 2.9.0.pr3 + + + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/FirebaseEventProxy.java b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/FirebaseEventProxy.java new file mode 100644 index 00000000000..4a115b74c5d --- /dev/null +++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/FirebaseEventProxy.java @@ -0,0 +1,114 @@ +/* + * Copyright 2016 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.example.gaefirebaseeventproxy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.appengine.api.utils.SystemProperty; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.ValueEventListener; + +import java.io.FileInputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +public class FirebaseEventProxy { + + private static final Logger log = Logger.getLogger(FirebaseEventProxy.class.getName()); + + public FirebaseEventProxy() { + String firebaseLocation = "https://crackling-torch-392.firebaseio.com"; + Map databaseAuthVariableOverride = new HashMap(); + // uid and provider will have to match what you have in your firebase security rules + databaseAuthVariableOverride.put("uid", "gae-firebase-event-proxy"); + databaseAuthVariableOverride.put("provider", "com.example"); + try { + FirebaseOptions options = new FirebaseOptions.Builder() + .setServiceAccount(new FileInputStream("gae-firebase-secrets.json")) + .setDatabaseUrl(firebaseLocation) + .setDatabaseAuthVariableOverride(databaseAuthVariableOverride).build(); + FirebaseApp.initializeApp(options); + } catch (IOException e) { + throw new RuntimeException( + "Error reading firebase secrets from file: src/main/webapp/gae-firebase-secrets.json: " + + e.getMessage()); + } + } + + public void start() { + DatabaseReference firebase = FirebaseDatabase.getInstance().getReference(); + + // Subscribe to value events. Depending on use case, you may want to subscribe to child events + // through childEventListener. + firebase.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (snapshot.exists()) { + try { + // Convert value to JSON using Jackson + String json = new ObjectMapper().writeValueAsString(snapshot.getValue(false)); + + // Replace the URL with the url of your own listener app. + URL dest = new URL("http://gae-firebase-listener-python.appspot.com/log"); + HttpURLConnection connection = (HttpURLConnection) dest.openConnection(); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + + // Rely on X-Appengine-Inbound-Appid to authenticate. Turning off redirects is + // required to enable. + connection.setInstanceFollowRedirects(false); + + // Fill out header if in dev environment + if (SystemProperty.environment.value() != SystemProperty.Environment.Value.Production) { + connection.setRequestProperty("X-Appengine-Inbound-Appid", "dev-instance"); + } + + // Put Firebase data into http request + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("&fbSnapshot="); + stringBuilder.append(URLEncoder.encode(json, "UTF-8")); + connection.getOutputStream().write(stringBuilder.toString().getBytes()); + if (connection.getResponseCode() != 200) { + log.severe("Forwarding failed"); + } else { + log.info("Sent: " + json); + } + } catch (JsonProcessingException e) { + log.severe("Unable to convert Firebase response to JSON: " + e.getMessage()); + } catch (IOException e) { + log.severe("Error in connecting to app engine: " + e.getMessage()); + } + } + } + + @Override + public void onCancelled(DatabaseError error) { + log.severe("Firebase connection cancelled: " + error.getMessage()); + } + }); + } +} diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/ServletContextListenerImpl.java b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/ServletContextListenerImpl.java new file mode 100644 index 00000000000..ca11ec2ba9a --- /dev/null +++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/java/com/example/GaeFirebaseEventProxy/ServletContextListenerImpl.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 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.example.gaefirebaseeventproxy; + +import java.util.logging.Logger; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +// ServletContextListener that is called whenever your App Engine app starts up. +public class ServletContextListenerImpl implements ServletContextListener { + + private static final Logger log = Logger.getLogger(ServletContextListener.class.getName()); + + @Override + public void contextInitialized(ServletContextEvent event) { + log.info("Starting ...."); + FirebaseEventProxy proxy = new FirebaseEventProxy(); + proxy.start(); + } + + @Override + public void contextDestroyed(ServletContextEvent event) { + // App Engine does not currently invoke this method. + } +} diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..5aa793d9834 --- /dev/null +++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,25 @@ + + + + java8 + true + + + 1 + + + + + + diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..a2cc700aef5 --- /dev/null +++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,27 @@ +# Copyright 2016 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. + +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# + +# Set the default logging level for all loggers to WARNING +.level = INFO diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/web.xml b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..5c9f392b35e --- /dev/null +++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,25 @@ + + + + + + com.example.gaefirebaseeventproxy.ServletContextListenerImpl + + + index.jsp + + diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/index.jsp b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/index.jsp new file mode 100644 index 00000000000..1caf98ad673 --- /dev/null +++ b/appengine-java8/firebase-event-proxy/gae-firebase-event-proxy/src/main/webapp/index.jsp @@ -0,0 +1,25 @@ +<%-- +Copyright 2015 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. +--%> + +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + + + + +

Status: up

+ + + diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/.gitignore b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/.gitignore new file mode 100644 index 00000000000..0d20b6487c6 --- /dev/null +++ b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/app.yaml b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/app.yaml new file mode 100644 index 00000000000..f041d384c05 --- /dev/null +++ b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +api_version: 1 +threadsafe: true + +handlers: +- url: /.* + script: main.app diff --git a/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/main.py b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/main.py new file mode 100644 index 00000000000..50990be57b0 --- /dev/null +++ b/appengine-java8/firebase-event-proxy/gae-firebase-listener-python/main.py @@ -0,0 +1,38 @@ +# Copyright 2016 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. + +import os +import webapp2 + +IS_DEV = os.environ["SERVER_SOFTWARE"][:3] == "Dev" +allowed_users = set() +if IS_DEV: + allowed_users.add("dev-instance") +else: + # Add your Java App Engine proxy App Id here + allowed_users.add("your-java-appengine-proxy-app-id") + +class LoggingHandler(webapp2.RequestHandler): + + def post(self): + user = self.request.headers.get('X-Appengine-Inbound-Appid', None) + if user and user in allowed_users: + firebaseSnapshot = self.request.params['fbSnapshot'] + print firebaseSnapshot + else: + print "Got unauthenticated user: %s" % user + +app = webapp2.WSGIApplication([ + webapp2.Route('/log', LoggingHandler), +]) diff --git a/appengine-java8/firebase-tictactoe/README.md b/appengine-java8/firebase-tictactoe/README.md new file mode 100644 index 00000000000..fc7fbf0d052 --- /dev/null +++ b/appengine-java8/firebase-tictactoe/README.md @@ -0,0 +1,53 @@ +# Tic Tac Toe on Google App Engine Standard using Firebase + +This directory contains a project that implements a realtime two-player game of +Tic Tac Toe on Google [App Engine Standard][standard], using the [Firebase] database +for realtime notifications when the board changes. + +[Firebase]: https://firebase.google.com +[standard]: https://cloud.google.com/appengine/docs/about-the-standard-environment + +## Prerequisites + +* Install [Apache Maven][maven] 3.5.0 or later +* Install the [Google Cloud SDK][sdk] +* Create a project in the [Firebase Console][fb-console] +* In the [Overview section][fb-overview] of the Firebase console, click 'Add + Firebase to your web app' and replace the contents of the file + `src/main/webapp/WEB-INF/view/firebase_config.jspf` with that code snippet. + +[fb-console]: https://console.firebase.google.com +[sdk]: https://cloud.google.com/sdk +[creds]: https://console.firebase.google.com/iam-admin/serviceaccounts/project?project=_&consoleReturnUrl=https:%2F%2Fconsole.firebase.google.com%2Fproject%2F_%2Fsettings%2Fgeneral%2F +[fb-overview]: https://console.firebase.google.com/project/_/overview +[maven]: https://maven.apache.org + + +## Run the sample + +* To run the app locally using the development appserver: + +```sh +mvn appengine:run +``` + +## Troubleshooting + +* If you see the error `Google Cloud SDK path was not provided ...`: + * Make sure you've installed the [Google Cloud SDK][sdk] + * Make sure the Google Cloud SDK's `bin/` directory is in your `PATH`. If + you prefer it not to be, you can also set the environment variable + `GOOGLE_CLOUD_SDK_HOME` to point to where you installed the SDK: + +```sh +export GOOGLE_CLOUD_SDK_HOME=/path/to/google-cloud-sdk +``` + +## Contributing changes + +See [CONTRIBUTING.md](../../CONTRIBUTING.md). + +## Licensing + +See [LICENSE](../../LICENSE). + diff --git a/appengine-java8/firebase-tictactoe/pom.xml b/appengine-java8/firebase-tictactoe/pom.xml new file mode 100644 index 00000000000..ae23e13cb6d --- /dev/null +++ b/appengine-java8/firebase-tictactoe/pom.xml @@ -0,0 +1,127 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-firebase-tictactoe-j8 + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + 5.1.17 + + 2.7 + 20.0 + 1.22.0 + 4.12 + 1.10.19 + 0.32 + 1.3.1 + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + com.google.code.gson + gson + 2.8.0 + + + com.googlecode.objectify + objectify + ${objectify.version} + + + com.google.guava + guava + ${guava.version} + + + com.google.api-client + google-api-client-appengine + ${google-api-client.version} + + + + + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-all + ${mockito.version} + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + ${google-truth.version} + test + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + ${appengine-maven.version} + + + + diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java new file mode 100644 index 00000000000..1988424fbb6 --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyService; + +import java.io.IOException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Handler that deletes the Firebase database that serves as the realtime communication channel. + * This handler should be invoked after a game has finished, to clean up the channel. + */ +public class DeleteServlet extends HttpServlet { + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws IOException { + String gameId = request.getParameter("gameKey"); + Objectify ofy = ObjectifyService.ofy(); + Game game = ofy.load().type(Game.class).id(gameId).safe(); + + UserService userService = UserServiceFactory.getUserService(); + String currentUserId = userService.getCurrentUser().getUserId(); + + // TODO(you): In practice, first validate that the user has permission to delete the Game + game.deleteChannel(currentUserId); + } +} diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java new file mode 100644 index 00000000000..5c859d65dbe --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/FirebaseChannel.java @@ -0,0 +1,235 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.extensions.appengine.http.UrlFetchTransport; +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.http.ByteArrayContent; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.appengine.api.appidentity.AppIdentityService; +import com.google.appengine.api.appidentity.AppIdentityServiceFactory; +import com.google.common.io.BaseEncoding; +import com.google.common.io.CharStreams; +import com.google.gson.Gson; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Utility functions for communicating with the realtime communication channel using Firebase. + * In this app, we use Firebase as a communication bus to push the state of the board to all clients + * - that is, players of the game. This class contains the methods used to communicate with + * Firebase. + */ +public class FirebaseChannel { + private static final String FIREBASE_SNIPPET_PATH = "WEB-INF/view/firebase_config.jspf"; + static InputStream firebaseConfigStream = null; + private static final Collection FIREBASE_SCOPES = Arrays.asList( + "https://www.googleapis.com/auth/firebase.database", + "https://www.googleapis.com/auth/userinfo.email" + ); + private static final String IDENTITY_ENDPOINT = + "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"; + + private String firebaseDbUrl; + private GoogleCredential credential; + // Keep this a package-private member variable, so that it can be mocked for unit tests + HttpTransport httpTransport; + + private static FirebaseChannel instance; + + /** + * FirebaseChannel is a singleton, since it's just utility functions. + * The class derives auth information when first instantiated. + */ + public static FirebaseChannel getInstance() { + if (instance == null) { + instance = new FirebaseChannel(); + } + return instance; + } + + /** + * Construct the singleton, with derived auth information. The Firebase database url is derived + * from the snippet that we provide to the client code, to guarantee that the client and the + * server are communicating with the same Firebase database. The auth credentials we'll use to + * communicate with Firebase is derived from App Engine's default credentials, and given + * Firebase's OAuth scopes. + */ + private FirebaseChannel() { + try { + // This variables exist primarily so it can be stubbed out in unit tests. + if (null == firebaseConfigStream) { + firebaseConfigStream = new FileInputStream(FIREBASE_SNIPPET_PATH); + } + + String firebaseSnippet = CharStreams.toString(new InputStreamReader( + firebaseConfigStream, StandardCharsets.UTF_8)); + firebaseDbUrl = parseFirebaseUrl(firebaseSnippet); + + credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES); + httpTransport = UrlFetchTransport.getDefaultInstance(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Parses out the Firebase database url from the client-side code snippet. + * The code snippet is a piece of javascript that defines an object with the key 'databaseURL'. So + * look for that key, then parse out its quote-surrounded value. + */ + private static String parseFirebaseUrl(String firebaseSnippet) { + int idx = firebaseSnippet.indexOf("databaseURL"); + if (-1 == idx) { + throw new RuntimeException( + "Please copy your Firebase web snippet into " + FIREBASE_SNIPPET_PATH); + } + idx = firebaseSnippet.indexOf(':', idx); + int openQuote = firebaseSnippet.indexOf('"', idx); + int closeQuote = firebaseSnippet.indexOf('"', openQuote + 1); + return firebaseSnippet.substring(openQuote + 1, closeQuote); + } + + public void sendFirebaseMessage(String channelKey, Game game) + throws IOException { + // Make requests auth'ed using Application Default Credentials + HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential); + GenericUrl url = new GenericUrl( + String.format("%s/channels/%s.json", firebaseDbUrl, channelKey)); + HttpResponse response = null; + + try { + if (null == game) { + response = requestFactory.buildDeleteRequest(url).execute(); + } else { + String gameJson = new Gson().toJson(game); + response = requestFactory.buildPatchRequest( + url, new ByteArrayContent("application/json", gameJson.getBytes())).execute(); + } + + if (response.getStatusCode() != 200) { + throw new RuntimeException( + "Error code while updating Firebase: " + response.getStatusCode()); + } + + } finally { + if (null != response) { + response.disconnect(); + } + } + } + + /** + * Create a secure JWT token for the given userId. + */ + public String createFirebaseToken(Game game, String userId) { + final AppIdentityService appIdentity = AppIdentityServiceFactory.getAppIdentityService(); + final BaseEncoding base64 = BaseEncoding.base64(); + + String header = base64.encode("{\"typ\":\"JWT\",\"alg\":\"RS256\"}".getBytes()); + + // Construct the claim + String channelKey = game.getChannelKey(userId); + String clientEmail = appIdentity.getServiceAccountName(); + long epochTime = System.currentTimeMillis() / 1000; + long expire = epochTime + 60 * 60; // an hour from now + + Map claims = new HashMap(); + claims.put("iss", clientEmail); + claims.put("sub", clientEmail); + claims.put("aud", IDENTITY_ENDPOINT); + claims.put("uid", channelKey); + claims.put("iat", epochTime); + claims.put("exp", expire); + + String payload = base64.encode(new Gson().toJson(claims).getBytes()); + String toSign = String.format("%s.%s", header, payload); + AppIdentityService.SigningResult result = appIdentity.signForApp(toSign.getBytes()); + return String.format("%s.%s", toSign, base64.encode(result.getSignature())); + } + + // The following methods are to illustrate making various calls to Firebase from App Engine + // Standard + + public HttpResponse firebasePut(String path, Object object) throws IOException { + // Make requests auth'ed using Application Default Credentials + Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES); + HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential); + + String json = new Gson().toJson(object); + GenericUrl url = new GenericUrl(path); + + return requestFactory.buildPutRequest( + url, new ByteArrayContent("application/json", json.getBytes())).execute(); + } + + public HttpResponse firebasePatch(String path, Object object) throws IOException { + // Make requests auth'ed using Application Default Credentials + Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES); + HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential); + + String json = new Gson().toJson(object); + GenericUrl url = new GenericUrl(path); + + return requestFactory.buildPatchRequest( + url, new ByteArrayContent("application/json", json.getBytes())).execute(); + } + + public HttpResponse firebasePost(String path, Object object) throws IOException { + // Make requests auth'ed using Application Default Credentials + Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES); + HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential); + + String json = new Gson().toJson(object); + GenericUrl url = new GenericUrl(path); + + return requestFactory.buildPostRequest( + url, new ByteArrayContent("application/json", json.getBytes())).execute(); + } + + public HttpResponse firebaseGet(String path) throws IOException { + // Make requests auth'ed using Application Default Credentials + Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES); + HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential); + + GenericUrl url = new GenericUrl(path); + + return requestFactory.buildGetRequest(url).execute(); + } + + public HttpResponse firebaseDelete(String path) throws IOException { + // Make requests auth'ed using Application Default Credentials + Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES); + HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential); + + GenericUrl url = new GenericUrl(path); + + return requestFactory.buildDeleteRequest(url).execute(); + } +} diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/Game.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/Game.java new file mode 100644 index 00000000000..7f6a1e84135 --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/Game.java @@ -0,0 +1,183 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; + +import java.io.IOException; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** + * The datastore-persisted Game object. This holds the entire game state - from a representation of + * the board, to the players are and whose turn it is, and who the winner is and how they won. + * + * It also contains some convenience functions for communicating updates to the board to the + * clients, via Firebase. + */ +@Entity +public class Game { + static final Pattern[] XWins = + {Pattern.compile("XXX......"), Pattern.compile("...XXX..."), Pattern.compile("......XXX"), + Pattern.compile("X..X..X.."), Pattern.compile(".X..X..X."), + Pattern.compile("..X..X..X"), Pattern.compile("X...X...X"), + Pattern.compile("..X.X.X..")}; + static final Pattern[] OWins = + {Pattern.compile("OOO......"), Pattern.compile("...OOO..."), Pattern.compile("......OOO"), + Pattern.compile("O..O..O.."), Pattern.compile(".O..O..O."), + Pattern.compile("..O..O..O"), Pattern.compile("O...O...O"), + Pattern.compile("..O.O.O..")}; + + @Id + public String id; + public String userX; + public String userO; + public String board; + public Boolean moveX; + public String winner; + public String winningBoard; + + private static final Logger LOGGER = Logger.getLogger(Game.class.getName()); + + Game() { + this.id = UUID.randomUUID().toString(); + } + + Game(String userX, String userO, String board, boolean moveX) { + this.id = UUID.randomUUID().toString(); + this.userX = userX; + this.userO = userO; + this.board = board; + this.moveX = moveX; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUserX() { + return userX; + } + + public String getUserO() { + return userO; + } + + public void setUserO(String userO) { + this.userO = userO; + } + + public String getBoard() { + return board; + } + + public void setBoard(String board) { + this.board = board; + } + + public boolean getMoveX() { + return moveX; + } + + public void setMoveX(boolean moveX) { + this.moveX = moveX; + } + + // [START send_updates] + public String getChannelKey(String userId) { + return userId + id; + } + + public void deleteChannel(String userId) + throws IOException { + if (userId != null) { + String channelKey = getChannelKey(userId); + FirebaseChannel.getInstance().sendFirebaseMessage(channelKey, null); + } + } + + private void sendUpdateToUser(String userId) + throws IOException { + if (userId != null) { + String channelKey = getChannelKey(userId); + FirebaseChannel.getInstance().sendFirebaseMessage(channelKey, this); + } + } + + public void sendUpdateToClients() + throws IOException { + sendUpdateToUser(userX); + sendUpdateToUser(userO); + } + // [END send_updates] + + public void checkWin() { + final Pattern[] wins; + if (moveX) { + wins = XWins; + } else { + wins = OWins; + } + + for (Pattern winPattern : wins) { + if (winPattern.matcher(board).matches()) { + if (moveX) { + winner = userX; + } else { + winner = userO; + } + winningBoard = winPattern.toString(); + } + } + } + + public boolean makeMove(int position, String userId) { + String currentMovePlayer; + char value; + if (getMoveX()) { + value = 'X'; + currentMovePlayer = getUserX(); + } else { + value = 'O'; + currentMovePlayer = getUserO(); + } + + if (currentMovePlayer.equals(userId)) { + char[] boardBytes = getBoard().toCharArray(); + boardBytes[position] = value; + setBoard(new String(boardBytes)); + checkWin(); + setMoveX(!getMoveX()); + try { + sendUpdateToClients(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error sending Game update to Firebase", e); + throw new RuntimeException(e); + } + return true; + } + + return false; + } +} diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java new file mode 100644 index 00000000000..4911d22bc37 --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java @@ -0,0 +1,52 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyService; + +import java.io.IOException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Handler for a user making a move in a game. + * Updates the game board with the requested move (if it's legal), and communicate the updated board + * to the clients. + */ +public class MoveServlet extends HttpServlet { + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws IOException { + String gameId = request.getParameter("gameKey"); + Objectify ofy = ObjectifyService.ofy(); + Game game = ofy.load().type(Game.class).id(gameId).safe(); + + UserService userService = UserServiceFactory.getUserService(); + String currentUserId = userService.getCurrentUser().getUserId(); + + int cell = new Integer(request.getParameter("cell")); + if (!game.makeMove(cell, currentUserId)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } else { + ofy.save().entity(game).now(); + } + } +} diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/ObjectifyHelper.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/ObjectifyHelper.java new file mode 100644 index 00000000000..abc1a28e33c --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/ObjectifyHelper.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import com.googlecode.objectify.ObjectifyService; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +/** + * ObjectifyHelper, a ServletContextListener, is setup in web.xml to run before a JSP is run. This + * is required to let JSP's access Ofy. + **/ +public class ObjectifyHelper implements ServletContextListener { + public void contextInitialized(ServletContextEvent event) { + // This will be invoked as part of a warmup request, or the first user request if no warmup + // request. + ObjectifyService.register(Game.class); + } + + public void contextDestroyed(ServletContextEvent event) { + // App Engine does not currently invoke this method. + } +} diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java new file mode 100644 index 00000000000..2e71f9391cb --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyService; + +import java.io.IOException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Handler that signals to all players of a game that the game has started. + */ +public class OpenedServlet extends HttpServlet { + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws IOException { + // TODO(you): In practice, you should validate the user has permission to post to the given Game + String gameId = request.getParameter("gameKey"); + Objectify ofy = ObjectifyService.ofy(); + Game game = ofy.load().type(Game.class).id(gameId).safe(); + game.sendUpdateToClients(); + } +} diff --git a/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java new file mode 100644 index 00000000000..8b248577971 --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java @@ -0,0 +1,106 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import com.google.appengine.api.users.UserServiceFactory; +import com.google.gson.Gson; +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyService; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Base handler for the Tic Tac Toe game. + * This handler serves up the initial jsp page that is the game, and also creates the persistent + * game in the datastore, as well as the Firebase database to serve as the communication channel to + * the clients. + */ +@SuppressWarnings("serial") +public class TicTacToeServlet extends HttpServlet { + + private String getGameUriWithGameParam(HttpServletRequest request, String gameKey) { + try { + String query = ""; + if (gameKey != null) { + query = "gameKey=" + gameKey; + } + URI thisUri = new URI(request.getRequestURL().toString()); + URI uriWithOptionalGameParam = new URI( + thisUri.getScheme(), thisUri.getUserInfo(), thisUri.getHost(), + thisUri.getPort(), thisUri.getPath(), query, ""); + return uriWithOptionalGameParam.toString(); + } catch (URISyntaxException e) { + // This should never happen, since we're constructing the URI from a valid URI. + // Nonetheless, wrap it in a RuntimeException to placate java. + throw new RuntimeException(e); + } + } + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String gameKey = request.getParameter("gameKey"); + + // 1. Create or fetch a Game object from the datastore + Objectify ofy = ObjectifyService.ofy(); + Game game = null; + String userId = UserServiceFactory.getUserService().getCurrentUser().getUserId(); + if (gameKey != null) { + game = ofy.load().type(Game.class).id(gameKey).now(); + if (null == game) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + if (game.getUserO() == null && !userId.equals(game.getUserX())) { + game.setUserO(userId); + } + ofy.save().entity(game).now(); + } else { + // Initialize a new board. The board is represented as a String of 9 spaces, one for each + // blank spot on the tic-tac-toe board. + game = new Game(userId, null, " ", true); + ofy.save().entity(game).now(); + gameKey = game.getId(); + } + + // 2. Create this Game in the firebase db + game.sendUpdateToClients(); + + // 3. Inject a secure token into the client, so it can get game updates + + // [START pass_token] + // The 'Game' object exposes a method which creates a unique string based on the game's key + // and the user's id. + String token = FirebaseChannel.getInstance().createFirebaseToken(game, userId); + request.setAttribute("token", token); + + // 4. More general template values + request.setAttribute("game_key", gameKey); + request.setAttribute("me", userId); + request.setAttribute("channel_id", game.getChannelKey(userId)); + request.setAttribute("initial_message", new Gson().toJson(game)); + request.setAttribute("game_link", getGameUriWithGameParam(request, gameKey)); + request.getRequestDispatcher("/WEB-INF/view/index.jsp").forward(request, response); + // [END pass_token] + } +} diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..4697140720f --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,34 @@ + + + + java8 + true + + + + + + + + + + + + + + + diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..9b29028efa7 --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,27 @@ +# +# Copyright 2016 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. +# +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# +# Set the default logging level for all loggers to WARNING +.level=WARNING diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/firebase_config.jspf b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/firebase_config.jspf new file mode 100644 index 00000000000..25898c985af --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/firebase_config.jspf @@ -0,0 +1,3 @@ +REPLACE ME WITH YOUR FIREBASE WEBAPP CODE SNIPPET: + +https://console.firebase.google.com/project/_/overview diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/index.jsp b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/index.jsp new file mode 100644 index 00000000000..fab4ac6f0ec --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/view/index.jsp @@ -0,0 +1,59 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%-- + Copyright 2016 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. +--%> + + + + + <%@ include file="firebase_config.jspf" %> + + + + + + +
+

Firebase-enabled Tic Tac Toe

+
+ Waiting for another player to join.
+ Send them this link to play:
+ +
+
Your move! Click a square to place your piece.
+
Waiting for other player to move...
+
You won this game!
+
You lost this game.
+
+
+
+
+
+
+
+
+
+
+
+
+ Quick link to this game: "><%= request.getAttribute("game_link") %> +
+
+ + diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..772f32fc601 --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,77 @@ + + + + + index + + + + entire-app + /* + + + * + + + + TicTacToeServlet + com.example.appengine.firetactoe.TicTacToeServlet + + + TicTacToeServlet + /index + + + OpenedServlet + com.example.appengine.firetactoe.OpenedServlet + + + OpenedServlet + /opened + + + MoveServlet + com.example.appengine.firetactoe.MoveServlet + + + MoveServlet + /move + + + DeleteServlet + com.example.appengine.firetactoe.DeleteServlet + + + DeleteServlet + /delete + + + + ObjectifyFilter + com.googlecode.objectify.ObjectifyFilter + + + ObjectifyFilter + /* + + + com.example.appengine.firetactoe.ObjectifyHelper + + diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.css b/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.css new file mode 100644 index 00000000000..f314eab5b37 --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.css @@ -0,0 +1,82 @@ +body { + font-family: 'Helvetica'; +} + +#board { + width:152px; + height: 152px; + margin: 20px auto; +} + +#display-area { + text-align: center; +} + +#other-player, #your-move, #their-move, #you-won, #you-lost { + display: none; +} + +#display-area.waiting #other-player { + display: block; +} + +#display-area.waiting #board, #display-area.waiting #this-game { + display: none; +} +#display-area.won #you-won { + display: block; +} +#display-area.lost #you-lost { + display: block; +} +#display-area.your-move #your-move { + display: block; +} +#display-area.their-move #their-move { + display: block; +} + + +#this-game { + font-size: 9pt; +} + +div.cell { + float: left; + width: 50px; + height: 50px; + border: none; + margin: 0px; + padding: 0px; + box-sizing: border-box; + + line-height: 50px; + font-family: "Helvetica"; + font-size: 16pt; + text-align: center; +} + +.your-move div.cell:hover { + background: lightgrey; +} + +.your-move div.cell:empty:hover { + background: lightblue; + cursor: pointer; +} + +div.l { + border-right: 1pt solid black; +} + +div.r { + border-left: 1pt solid black; +} + +div.t { + border-bottom: 1pt solid black; +} + +div.b { + border-top: 1pt solid black; +} diff --git a/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.js b/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.js new file mode 100644 index 00000000000..6ae24b0247d --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/main/webapp/static/main.js @@ -0,0 +1,178 @@ +/** + * Copyright 2016 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. + */ + +'use strict'; + +/** + * @fileoverview Tic-Tac-Toe, using the Firebase API + */ + +/** + * @param gameKey - a unique key for this game. + * @param me - my user id. + * @param token - secure token passed from the server + * @param channelId - id of the 'channel' we'll be listening to + */ +function initGame(gameKey, me, token, channelId, initialMessage) { + var state = { + gameKey: gameKey, + me: me + }; + + // This is our Firebase realtime DB path that we'll listen to for updates + // We'll initialize this later in openChannel() + var channel = null; + + /** + * Updates the displayed game board. + */ + function updateGame(newState) { + $.extend(state, newState); + + $('.cell').each(function(i) { + var square = $(this); + var value = state.board[i]; + square.html(' ' === value ? '' : value); + + if (state.winner && state.winningBoard) { + if (state.winningBoard[i] === value) { + if (state.winner === state.me) { + square.css('background', 'green'); + } else { + square.css('background', 'red'); + } + } else { + square.css('background', ''); + } + } + }); + + var displayArea = $('#display-area'); + + if (!state.userO) { + displayArea[0].className = 'waiting'; + } else if (state.winner === state.me) { + displayArea[0].className = 'won'; + } else if (state.winner) { + displayArea[0].className = 'lost'; + } else if (isMyMove()) { + displayArea[0].className = 'your-move'; + } else { + displayArea[0].className = 'their-move'; + } + } + + function isMyMove() { + return !state.winner && (state.moveX === (state.userX === state.me)); + } + + function myPiece() { + return state.userX === state.me ? 'X' : 'O'; + } + + /** + * Send the user's latest move back to the server + */ + function moveInSquare(e) { + var id = $(e.currentTarget).index(); + if (isMyMove() && state.board[id] === ' ') { + $.post('/move', {cell: id}); + } + } + + /** + * This method lets the server know that the user has opened the channel + * After this method is called, the server may begin to send updates + */ + function onOpened() { + $.post('/opened'); + } + + /** + * This deletes the data associated with the Firebase path + * it is critical that this data be deleted since it costs money + */ + function deleteChannel() { + $.post('/delete'); + } + + /** + * This method is called every time an event is fired from Firebase + * it updates the entire game state and checks for a winner + * if a player has won the game, this function calls the server to delete + * the data stored in Firebase + */ + function onMessage(newState) { + updateGame(newState); + + // now check to see if there is a winner + if (channel && state.winner && state.winningBoard) { + channel.off(); //stop listening on this path + deleteChannel(); //delete the data we wrote + } + } + + /** + * This function opens a realtime communication channel with Firebase + * It logs in securely using the client token passed from the server + * then it sets up a listener on the proper database path (also passed by server) + * finally, it calls onOpened() to let the server know it is ready to receive messages + */ + function openChannel() { + // [START auth_login] + // sign into Firebase with the token passed from the server + firebase.auth().signInWithCustomToken(token).catch(function(error) { + console.log('Login Failed!', error.code); + console.log('Error message: ', error.message); + }); + // [END auth_login] + + // [START add_listener] + // setup a database reference at path /channels/channelId + channel = firebase.database().ref('channels/' + channelId); + // add a listener to the path that fires any time the value of the data changes + channel.on('value', function(data) { + onMessage(data.val()); + }); + // [END add_listener] + onOpened(); + // let the server know that the channel is open + } + + /** + * This function opens a communication channel with the server + * then it adds listeners to all the squares on the board + * next it pulls down the initial game state from template values + * finally it updates the game state with those values by calling onMessage() + */ + function initialize() { + // Always include the gamekey in our requests + $.ajaxPrefilter(function(opts) { + if (opts.url.indexOf('?') > 0) + opts.url += '&gameKey=' + state.gameKey; + else + opts.url += '?gameKey=' + state.gameKey; + }); + + $('#board').on('click', '.cell', moveInSquare); + + openChannel(); + + onMessage(initialMessage); + } + + setTimeout(initialize, 100); +} diff --git a/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/DeleteServletTest.java b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/DeleteServletTest.java new file mode 100644 index 00000000000..0df68aaaefc --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/DeleteServletTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig; +import com.google.common.collect.ImmutableMap; +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyFactory; +import com.googlecode.objectify.ObjectifyService; +import com.googlecode.objectify.util.Closeable; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashMap; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link DeleteServlet}. + */ +@RunWith(JUnit4.class) +public class DeleteServletTest { + private static final String USER_EMAIL = "whisky@tangofoxtr.ot"; + private static final String USER_ID = "whiskytangofoxtrot"; + private static final String FIREBASE_DB_URL = "http://firebase.com/dburl"; + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0), + new LocalUserServiceTestConfig(), + new LocalURLFetchServiceTestConfig() + ) + .setEnvEmail(USER_EMAIL) + .setEnvAuthDomain("gmail.com") + .setEnvAttributes(new HashMap( + ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID))); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + protected Closeable dbSession; + + private DeleteServlet servletUnderTest; + + @BeforeClass + public static void setUpBeforeClass() { + // Reset the Factory so that all translators work properly. + ObjectifyService.setFactory(new ObjectifyFactory()); + ObjectifyService.register(Game.class); + // Mock out the firebase config + FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream( + String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes()); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + dbSession = ObjectifyService.begin(); + + servletUnderTest = new DeleteServlet(); + + helper.setEnvIsLoggedIn(true); + // Make sure there are no firebase requests if we don't expect it + FirebaseChannel.getInstance().httpTransport = null; + } + + @After + public void tearDown() { + dbSession.close(); + helper.tearDown(); + } + + @Test + public void doPost_noGameKey() throws Exception { + try { + servletUnderTest.doPost(mockRequest, mockResponse); + fail("Should not succeed with no gameKey specified."); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).startsWith("id 'null'"); + } + } + + @Test + public void doPost_deleteGame() throws Exception { + // Insert a game + Objectify ofy = ObjectifyService.ofy(); + Game game = new Game(USER_ID, "my-opponent", " ", true); + ofy.save().entity(game).now(); + String gameKey = game.getId(); + when(mockRequest.getParameter("gameKey")).thenReturn(gameKey); + + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + + servletUnderTest.doPost(mockRequest, mockResponse); + + verify(mockHttpTransport, times(1)).buildRequest( + eq("DELETE"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$")); + } +} diff --git a/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/FirebaseChannelTest.java b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/FirebaseChannelTest.java new file mode 100644 index 00000000000..0c92863f77e --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/FirebaseChannelTest.java @@ -0,0 +1,254 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.appengine.tools.development.testing.LocalAppIdentityServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * Unit tests for {@link FirebaseChannel}. + */ +@RunWith(JUnit4.class) +public class FirebaseChannelTest { + private static final String FIREBASE_DB_URL = "http://firebase.com/dburl"; + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalAppIdentityServiceTestConfig()); + + private static FirebaseChannel firebaseChannel; + + @BeforeClass + public static void setUpBeforeClass() { + // Mock out the firebase config + FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream( + String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes()); + + firebaseChannel = FirebaseChannel.getInstance(); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void sendFirebaseMessage_create() throws Exception { + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + + firebaseChannel.sendFirebaseMessage("my_key", new Game()); + + verify(mockHttpTransport, times(1)).buildRequest( + "PATCH", FIREBASE_DB_URL + "/channels/my_key.json"); + } + + @Test + public void sendFirebaseMessage_delete() throws Exception { + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + + firebaseChannel.sendFirebaseMessage("my_key", null); + + verify(mockHttpTransport, times(1)).buildRequest( + "DELETE", FIREBASE_DB_URL + "/channels/my_key.json"); + } + + @Test + public void createFirebaseToken() throws Exception { + Game game = new Game(); + + String jwt = firebaseChannel.createFirebaseToken(game, "userId"); + + assertThat(jwt).matches("^([\\w+/=-]+\\.){2}[\\w+/=-]+$"); + } + + @Test + public void firebasePut() throws Exception { + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + Game game = new Game(); + + firebaseChannel.firebasePut(FIREBASE_DB_URL + "/my/path", game); + + verify(mockHttpTransport, times(1)).buildRequest("PUT", FIREBASE_DB_URL + "/my/path"); + } + + @Test + public void firebasePatch() throws Exception { + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + Game game = new Game(); + + firebaseChannel.firebasePatch(FIREBASE_DB_URL + "/my/path", game); + + verify(mockHttpTransport, times(1)).buildRequest("PATCH", FIREBASE_DB_URL + "/my/path"); + } + + @Test + public void firebasePost() throws Exception { + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + Game game = new Game(); + + firebaseChannel.firebasePost(FIREBASE_DB_URL + "/my/path", game); + + verify(mockHttpTransport, times(1)).buildRequest("POST", FIREBASE_DB_URL + "/my/path"); + } + + @Test + public void firebaseGet() throws Exception { + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + + firebaseChannel.firebaseGet(FIREBASE_DB_URL + "/my/path"); + + verify(mockHttpTransport, times(1)).buildRequest("GET", FIREBASE_DB_URL + "/my/path"); + } + + @Test + public void firebaseDelete() throws Exception { + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + + firebaseChannel.firebaseDelete(FIREBASE_DB_URL + "/my/path"); + + verify(mockHttpTransport, times(1)).buildRequest("DELETE", FIREBASE_DB_URL + "/my/path"); + } +} diff --git a/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/MoveServletTest.java b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/MoveServletTest.java new file mode 100644 index 00000000000..ac07a251028 --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/MoveServletTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig; +import com.google.common.collect.ImmutableMap; +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyFactory; +import com.googlecode.objectify.ObjectifyService; +import com.googlecode.objectify.util.Closeable; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashMap; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link MoveServlet}. + */ +@RunWith(JUnit4.class) +public class MoveServletTest { + private static final String USER_EMAIL = "whisky@tangofoxtr.ot"; + private static final String USER_ID = "whiskytangofoxtrot"; + private static final String FIREBASE_DB_URL = "http://firebase.com/dburl"; + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0), + new LocalUserServiceTestConfig(), + new LocalURLFetchServiceTestConfig() + ) + .setEnvEmail(USER_EMAIL) + .setEnvAuthDomain("gmail.com") + .setEnvAttributes(new HashMap( + ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID))); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + protected Closeable dbSession; + + private MoveServlet servletUnderTest; + + @BeforeClass + public static void setUpBeforeClass() { + // Reset the Factory so that all translators work properly. + ObjectifyService.setFactory(new ObjectifyFactory()); + ObjectifyService.register(Game.class); + // Mock out the firebase config + FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream( + String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes()); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + dbSession = ObjectifyService.begin(); + + servletUnderTest = new MoveServlet(); + + helper.setEnvIsLoggedIn(true); + // Make sure there are no firebase requests if we don't expect it + FirebaseChannel.getInstance().httpTransport = null; + } + + @After + public void tearDown() { + dbSession.close(); + helper.tearDown(); + } + + @Test + public void doPost_myTurn_move() throws Exception { + // Insert a game + Objectify ofy = ObjectifyService.ofy(); + Game game = new Game(USER_ID, "my-opponent", " ", true); + ofy.save().entity(game).now(); + String gameKey = game.getId(); + + when(mockRequest.getParameter("gameKey")).thenReturn(gameKey); + when(mockRequest.getParameter("cell")).thenReturn("1"); + + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + + servletUnderTest.doPost(mockRequest, mockResponse); + + game = ofy.load().type(Game.class).id(gameKey).safe(); + assertThat(game.board).isEqualTo(" X "); + + verify(mockHttpTransport, times(2)).buildRequest( + eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$")); + } + + public void doPost_notMyTurn_move() throws Exception { + // Insert a game + Objectify ofy = ObjectifyService.ofy(); + Game game = new Game(USER_ID, "my-opponent", " ", false); + ofy.save().entity(game).now(); + String gameKey = game.getId(); + + when(mockRequest.getParameter("gameKey")).thenReturn(gameKey); + when(mockRequest.getParameter("cell")).thenReturn("1"); + + servletUnderTest.doPost(mockRequest, mockResponse); + + verify(mockResponse).sendError(401); + } +} diff --git a/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/OpenedServletTest.java b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/OpenedServletTest.java new file mode 100644 index 00000000000..9b2990196f6 --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/OpenedServletTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig; +import com.google.common.collect.ImmutableMap; +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyFactory; +import com.googlecode.objectify.ObjectifyService; +import com.googlecode.objectify.util.Closeable; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashMap; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link OpenedServlet}. + */ +@RunWith(JUnit4.class) +public class OpenedServletTest { + private static final String USER_EMAIL = "whisky@tangofoxtr.ot"; + private static final String USER_ID = "whiskytangofoxtrot"; + private static final String FIREBASE_DB_URL = "http://firebase.com/dburl"; + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0), + new LocalUserServiceTestConfig(), + new LocalURLFetchServiceTestConfig() + ) + .setEnvEmail(USER_EMAIL) + .setEnvAuthDomain("gmail.com") + .setEnvAttributes(new HashMap( + ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID))); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + protected Closeable dbSession; + + private OpenedServlet servletUnderTest; + + @BeforeClass + public static void setUpBeforeClass() { + // Reset the Factory so that all translators work properly. + ObjectifyService.setFactory(new ObjectifyFactory()); + ObjectifyService.register(Game.class); + // Mock out the firebase config + FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream( + String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes()); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + dbSession = ObjectifyService.begin(); + + servletUnderTest = new OpenedServlet(); + + helper.setEnvIsLoggedIn(true); + // Make sure there are no firebase requests if we don't expect it + FirebaseChannel.getInstance().httpTransport = null; + } + + @After + public void tearDown() { + dbSession.close(); + helper.tearDown(); + } + + @Test + public void doPost_open() throws Exception { + // Insert a game + Objectify ofy = ObjectifyService.ofy(); + Game game = new Game(USER_ID, "my-opponent", " ", true); + ofy.save().entity(game).now(); + String gameKey = game.getId(); + + when(mockRequest.getParameter("gameKey")).thenReturn(gameKey); + + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + + servletUnderTest.doPost(mockRequest, mockResponse); + + verify(mockHttpTransport, times(2)).buildRequest( + eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$")); + } +} diff --git a/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java new file mode 100644 index 00000000000..7f5fdf90150 --- /dev/null +++ b/appengine-java8/firebase-tictactoe/src/test/java/com/example/appengine/firetactoe/TicTacToeServletTest.java @@ -0,0 +1,209 @@ +/* + * Copyright 2016 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.example.appengine.firetactoe; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig; +import com.google.common.collect.ImmutableMap; +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyFactory; +import com.googlecode.objectify.ObjectifyService; +import com.googlecode.objectify.util.Closeable; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.StringBuffer; +import java.util.HashMap; +import javax.servlet.RequestDispatcher; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link TicTacToeServlet}. + */ +@RunWith(JUnit4.class) +public class TicTacToeServletTest { + private static final String USER_EMAIL = "whisky@tangofoxtr.ot"; + private static final String USER_ID = "whiskytangofoxtrot"; + private static final String FIREBASE_DB_URL = "http://firebase.com/dburl"; + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // http://g.co/cloud/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig().setDefaultHighRepJobPolicyUnappliedJobPercentage(0), + new LocalUserServiceTestConfig(), + new LocalURLFetchServiceTestConfig() + ) + .setEnvEmail(USER_EMAIL) + .setEnvAuthDomain("gmail.com") + .setEnvAttributes(new HashMap( + ImmutableMap.of("com.google.appengine.api.users.UserService.user_id_key", USER_ID))); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + protected Closeable dbSession; + @Mock RequestDispatcher requestDispatcher; + + private TicTacToeServlet servletUnderTest; + + @BeforeClass + public static void setUpBeforeClass() { + // Reset the Factory so that all translators work properly. + ObjectifyService.setFactory(new ObjectifyFactory()); + ObjectifyService.register(Game.class); + // Mock out the firebase config + FirebaseChannel.firebaseConfigStream = new ByteArrayInputStream( + String.format("databaseURL: \"%s\"", FIREBASE_DB_URL).getBytes()); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + dbSession = ObjectifyService.begin(); + + // Set up a fake HTTP response. + when(mockRequest.getRequestURL()).thenReturn(new StringBuffer("https://timbre/")); + when(mockRequest.getRequestDispatcher("/WEB-INF/view/index.jsp")).thenReturn(requestDispatcher); + + servletUnderTest = new TicTacToeServlet(); + + helper.setEnvIsLoggedIn(true); + } + + @After + public void tearDown() { + dbSession.close(); + helper.tearDown(); + } + + @Test + public void doGet_noGameKey() throws Exception { + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + + servletUnderTest.doGet(mockRequest, mockResponse); + + // Make sure the game object was created for a new game + Objectify ofy = ObjectifyService.ofy(); + Game game = ofy.load().type(Game.class).first().safe(); + assertThat(game.userX).isEqualTo(USER_ID); + + verify(mockHttpTransport, times(1)).buildRequest( + eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$")); + verify(requestDispatcher).forward(mockRequest, mockResponse); + verify(mockRequest).setAttribute(eq("token"), anyString()); + verify(mockRequest).setAttribute("game_key", game.id); + verify(mockRequest).setAttribute("me", USER_ID); + verify(mockRequest).setAttribute("channel_id", USER_ID + game.id); + verify(mockRequest).setAttribute(eq("initial_message"), anyString()); + verify(mockRequest).setAttribute(eq("game_link"), anyString()); + } + + @Test + public void doGet_existingGame() throws Exception { + // Mock out the firebase response. See + // http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = spy(new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }); + FirebaseChannel.getInstance().httpTransport = mockHttpTransport; + + // Insert a game + Objectify ofy = ObjectifyService.ofy(); + Game game = new Game("some-other-user-id", null, " ", true); + ofy.save().entity(game).now(); + String gameKey = game.getId(); + + when(mockRequest.getParameter("gameKey")).thenReturn(gameKey); + + servletUnderTest.doGet(mockRequest, mockResponse); + + // Make sure the game object was updated with the other player + game = ofy.load().type(Game.class).first().safe(); + assertThat(game.userX).isEqualTo("some-other-user-id"); + assertThat(game.userO).isEqualTo(USER_ID); + + verify(mockHttpTransport, times(2)).buildRequest( + eq("PATCH"), Matchers.matches(FIREBASE_DB_URL + "/channels/[\\w-]+.json$")); + verify(requestDispatcher).forward(mockRequest, mockResponse); + verify(mockRequest).setAttribute(eq("token"), anyString()); + verify(mockRequest).setAttribute("game_key", game.id); + verify(mockRequest).setAttribute("me", USER_ID); + verify(mockRequest).setAttribute("channel_id", USER_ID + gameKey); + verify(mockRequest).setAttribute(eq("initial_message"), anyString()); + verify(mockRequest).setAttribute(eq("game_link"), anyString()); + } + + @Test + public void doGet_nonExistentGame() throws Exception { + when(mockRequest.getParameter("gameKey")).thenReturn("does-not-exist"); + + servletUnderTest.doGet(mockRequest, mockResponse); + + verify(mockResponse).sendError(404); + } +} diff --git a/appengine-java8/guestbook-cloud-datastore/README.md b/appengine-java8/guestbook-cloud-datastore/README.md new file mode 100644 index 00000000000..feee3374a1e --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/README.md @@ -0,0 +1,26 @@ +# appengine/guestbook-cloud-datastore + +An App Engine guestbook using Java, Maven, and the Cloud Datastore API via +[google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java). + +Please ask questions on [StackOverflow](http://stackoverflow.com/questions/tagged/google-app-engine). + +## Running Locally + +First, pick a project ID. You can create a project in the [Cloud Console] if you'd like, though this +isn't necessary unless you'd like to deploy the sample. + +Second, modify `Persistence.java`: replace `your-project-id-here` with the project ID you picked. + +Then start the [Cloud Datastore Emulator](https://cloud.google.com/datastore/docs/tools/datastore-emulator): + + gcloud beta emulators datastore start --project=YOUR_PROJECT_ID_HERE + +Finally, in a new shell, [set the Datastore Emulator environmental variables](https://cloud.google.com/datastore/docs/tools/datastore-emulator#setting_environment_variables) +and run + + mvn clean appengine:run + +## Deploying + + mvn clean appengine:deploy diff --git a/appengine-java8/guestbook-cloud-datastore/pom.xml b/appengine-java8/guestbook-cloud-datastore/pom.xml new file mode 100644 index 00000000000..2a57190c0da --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/pom.xml @@ -0,0 +1,113 @@ + + + + + 4.0.0 + war + 1.0-SNAPSHOT + + com.example.appengine + appengine-guestbook-cloud-datastore-j8 + + 19.0 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + jstl + jstl + 1.2 + + + + com.google.cloud + google-cloud + 0.17.2-alpha + + + + com.google.guava + guava + ${guava.version} + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + + + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + + + diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Greeting.java b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Greeting.java new file mode 100644 index 00000000000..c4ae6783399 --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Greeting.java @@ -0,0 +1,124 @@ +/** + * Copyright 2016 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. + */ + +//[START all] +package com.example.guestbook; + +import static com.example.guestbook.Persistence.getDatastore; + +import com.google.cloud.Timestamp; +import com.google.cloud.datastore.Entity; +import com.google.cloud.datastore.FullEntity; +import com.google.cloud.datastore.FullEntity.Builder; +import com.google.cloud.datastore.IncompleteKey; +import com.google.cloud.datastore.Key; +import com.google.common.base.MoreObjects; + +import java.util.Date; +import java.util.Objects; + +public class Greeting { + private Guestbook book; + + public Key key; + public String authorEmail; + public String authorId; + public String content; + public Date date; + + public Greeting() { + date = new Date(); + } + + public Greeting(String book, String content) { + this(); + this.book = new Guestbook(book); + this.content = content; + } + + public Greeting(String book, String content, String id, String email) { + this(book, content); + authorEmail = email; + authorId = id; + } + + public Greeting(Entity entity) { + key = entity.hasKey() ? entity.getKey() : null; + authorEmail = entity.contains("authorEmail") ? entity.getString("authorEmail") : null; + authorId = entity.contains("authorId") ? entity.getString("authorId") : null; + + date = entity.contains("date") ? entity.getTimestamp("date").toSqlTimestamp() : null; + content = entity.contains("content") ? entity.getString("content") : null; + } + + public void save() { + if (key == null) { + key = getDatastore().allocateId(makeIncompleteKey()); // Give this greeting a unique ID + } + + Builder builder = FullEntity.newBuilder(key); + + if (authorEmail != null) { + builder.set("authorEmail", authorEmail); + } + + if (authorId != null) { + builder.set("authorId", authorId); + } + + builder.set("content", content); + builder.set("date", Timestamp.of(date)); + + getDatastore().put(builder.build()); + } + + private IncompleteKey makeIncompleteKey() { + // The book is our ancestor key. + return Key.newBuilder(book.getKey(), "Greeting").build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Greeting greeting = (Greeting) o; + return Objects.equals(key, greeting.key) + && Objects.equals(authorEmail, greeting.authorEmail) + && Objects.equals(authorId, greeting.authorId) + && Objects.equals(content, greeting.content) + && Objects.equals(date, greeting.date); + } + + @Override + public int hashCode() { + return Objects.hash(key, authorEmail, authorId, content, date); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("key", key) + .add("authorEmail", authorEmail) + .add("authorId", authorId) + .add("content", content) + .add("date", date) + .add("book", book) + .toString(); + } +} +//[END all] diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Guestbook.java b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Guestbook.java new file mode 100644 index 00000000000..4f2aeb69a5d --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Guestbook.java @@ -0,0 +1,98 @@ +/** + * Copyright 2016 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.example.guestbook; + +import static com.example.guestbook.Persistence.getDatastore; +import static com.example.guestbook.Persistence.getKeyFactory; +import static com.google.cloud.datastore.StructuredQuery.OrderBy.desc; +import static com.google.cloud.datastore.StructuredQuery.PropertyFilter.hasAncestor; + +import com.google.cloud.datastore.Entity; +import com.google.cloud.datastore.EntityQuery; +import com.google.cloud.datastore.Key; +import com.google.cloud.datastore.KeyFactory; +import com.google.cloud.datastore.Query; +import com.google.cloud.datastore.QueryResults; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +import java.util.List; +import java.util.Objects; + +//[START all] +public class Guestbook { + private static final KeyFactory keyFactory = getKeyFactory(Guestbook.class); + private final Key key; + + public final String book; + + public Guestbook(String book) { + this.book = book == null ? "default" : book; + key = + keyFactory.newKey( + this.book); // There is a 1:1 mapping between Guestbook names and Guestbook objects + } + + public Key getKey() { + return key; + } + + public List getGreetings() { + // This query requires the index defined in index.yaml to work because of the orderBy on date. + EntityQuery query = + Query.newEntityQueryBuilder() + .setKind("Greeting") + .setFilter(hasAncestor(key)) + .setOrderBy(desc("date")) + .setLimit(5) + .build(); + + QueryResults results = getDatastore().run(query); + + Builder resultListBuilder = ImmutableList.builder(); + while (results.hasNext()) { + resultListBuilder.add(new Greeting(results.next())); + } + + return resultListBuilder.build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Guestbook guestbook = (Guestbook) o; + return Objects.equals(book, guestbook.book) && Objects.equals(key, guestbook.key); + } + + @Override + public int hashCode() { + return Objects.hash(book, key); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("keyFactory", keyFactory) + .add("book", book) + .add("key", key) + .toString(); + } +} +//[END all] diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Persistence.java b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Persistence.java new file mode 100644 index 00000000000..1b68b8541e9 --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/Persistence.java @@ -0,0 +1,43 @@ +/** + * Copyright 2016 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.example.guestbook; + +import com.google.cloud.datastore.Datastore; +import com.google.cloud.datastore.DatastoreOptions; +import com.google.cloud.datastore.KeyFactory; + +import java.util.concurrent.atomic.AtomicReference; + +//[START all] +public class Persistence { + private static AtomicReference datastore = new AtomicReference<>(); + + public static Datastore getDatastore() { + if (datastore.get() == null) { + datastore.set(DatastoreOptions.newBuilder().setProjectId("your-project-id-here") + .build().getService()); + } + + return datastore.get(); + } + + public static void setDatastore(Datastore datastore) { + Persistence.datastore.set(datastore); + } + + public static KeyFactory getKeyFactory(Class c) { + return getDatastore().newKeyFactory().setKind(c.getSimpleName()); + } +} +//[END all] diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/SignGuestbookServlet.java b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/SignGuestbookServlet.java new file mode 100644 index 00000000000..2c95c5c3aee --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/main/java/com/example/guestbook/SignGuestbookServlet.java @@ -0,0 +1,50 @@ +/** + * Copyright 2016 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. + */ + +//[START all] +package com.example.guestbook; + +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; + +import java.io.IOException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +//[START all] +public class SignGuestbookServlet extends HttpServlet { + // Process the HTTP POST of the form + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + Greeting greeting; + + UserService userService = UserServiceFactory.getUserService(); + User user = userService.getCurrentUser(); // Find out who the user is. + + String guestbookName = req.getParameter("guestbookName"); + String content = req.getParameter("content"); + if (user != null) { + greeting = new Greeting(guestbookName, content, user.getUserId(), user.getEmail()); + } else { + greeting = new Greeting(guestbookName, content); + } + + greeting.save(); + + resp.sendRedirect("/guestbook.jsp?guestbookName=" + guestbookName); + } +} +//[END all] diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..21996da7d42 --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,9 @@ + + + java8 + true + + + + + diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/index.yaml b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/index.yaml new file mode 100644 index 00000000000..e9beac04f79 --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/index.yaml @@ -0,0 +1,7 @@ +indexes: + +- kind: Greeting + ancestor: yes + properties: + - name: date + direction: desc diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..c2a1d42755d --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,12 @@ +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# +# Set the default logging level for all loggers to WARNING +.level=WARNING diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/web.xml b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..110a5c42fbc --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,22 @@ + + + + + + sign + com.example.guestbook.SignGuestbookServlet + 1 + + + + sign + /sign + + + + guestbook.jsp + + + diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/webapp/guestbook.jsp b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/guestbook.jsp new file mode 100644 index 00000000000..163fd1a29b3 --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/guestbook.jsp @@ -0,0 +1,98 @@ +<%-- //[START all]--%> +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="com.google.appengine.api.users.User" %> +<%@ page import="com.google.appengine.api.users.UserService" %> +<%@ page import="com.google.appengine.api.users.UserServiceFactory" %> + +<%-- //[START imports]--%> +<%@ page import="com.example.guestbook.Greeting" %> +<%@ page import="com.example.guestbook.Guestbook" %> +<%-- //[END imports]--%> + +<%@ page import="java.util.List" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + + + + + + + + +<% + String guestbookName = request.getParameter("guestbookName"); + if (guestbookName == null) { + guestbookName = "default"; + } + pageContext.setAttribute("guestbookName", guestbookName); + UserService userService = UserServiceFactory.getUserService(); + User user = userService.getCurrentUser(); + if (user != null) { + pageContext.setAttribute("user", user); +%> + +

Hello, ${fn:escapeXml(user.nickname)}! (You can + sign out.)

+<% + } else { +%> +

Hello! + Sign in + to include your name with greetings you post.

+<% + } +%> + +<%-- //[START datastore]--%> +<% + // Create the correct Ancestor key + Guestbook theBook = new Guestbook(guestbookName); + + // Run an ancestor query to ensure we see the most up-to-date + // view of the Greetings belonging to the selected Guestbook. + List greetings = theBook.getGreetings(); + + if (greetings.isEmpty()) { +%> +

Guestbook '${fn:escapeXml(guestbookName)}' has no messages.

+<% + } else { +%> +

Messages in Guestbook '${fn:escapeXml(guestbookName)}'.

+<% + // Look at all of our greetings + for (Greeting greeting : greetings) { + pageContext.setAttribute("greeting_content", greeting.content); + String author; + if (greeting.authorEmail == null) { + author = "An anonymous person"; + } else { + author = greeting.authorEmail; + String author_id = greeting.authorId; + if (user != null && user.getUserId().equals(author_id)) { + author += " (You)"; + } + } + pageContext.setAttribute("greeting_user", author); +%> +

${fn:escapeXml(greeting_user)} wrote:

+
${fn:escapeXml(greeting_content)}
+<% + } + } +%> + +
+
+
+ +
+<%-- //[END datastore]--%> +
+
+
+
+ + + +<%-- //[END all]--%> diff --git a/appengine-java8/guestbook-cloud-datastore/src/main/webapp/stylesheets/main.css b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/stylesheets/main.css new file mode 100644 index 00000000000..05d72d5536d --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/main/webapp/stylesheets/main.css @@ -0,0 +1,4 @@ +body { + font-family: Verdana, Helvetica, sans-serif; + background-color: #FFFFCC; +} diff --git a/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/GreetingTest.java b/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/GreetingTest.java new file mode 100644 index 00000000000..a91390569ad --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/GreetingTest.java @@ -0,0 +1,50 @@ +/** + * Copyright 2016 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.example.guestbook; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.List; + +@RunWith(JUnit4.class) +public class GreetingTest { + + @Before + public void setUp() { + TestUtils.startDatastore(); + } + + @Test + public void testSaveGreeting() throws Exception { + Greeting greeting = new Greeting(null, "Test!"); + greeting.save(); + + Guestbook guestbook = new Guestbook(null); + List greetings = guestbook.getGreetings(); + assertTrue(greetings.size() == 1); + assertEquals(greeting, greetings.get(0)); + } + + @After + public void tearDown() { + TestUtils.stopDatastore(); + } +} diff --git a/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/SignGuestbookServletTest.java b/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/SignGuestbookServletTest.java new file mode 100644 index 00000000000..4a5435eafe7 --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/SignGuestbookServletTest.java @@ -0,0 +1,70 @@ +/** + * Copyright 2016 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.example.guestbook; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@RunWith(JUnit4.class) +public class SignGuestbookServletTest { + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + + private SignGuestbookServlet signGuestbookServlet; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + // Sets up the UserServiceFactory used in SignGuestbookServlet (but not in this test) + helper.setUp(); + + signGuestbookServlet = new SignGuestbookServlet(); + TestUtils.startDatastore(); + } + + @Test + public void doPost_userNotLoggedIn() throws Exception { + String testBook = "default"; + when(mockRequest.getParameter("guestbookName")).thenReturn(testBook); + String testGreeting = "beep!"; + when(mockRequest.getParameter("content")).thenReturn(testGreeting); + + signGuestbookServlet.doPost(mockRequest, mockResponse); + Guestbook guestbook = new Guestbook(testBook); + List greetings = guestbook.getGreetings(); + + assertTrue(greetings.size() == 1); + assertTrue(greetings.get(0).content.equals(testGreeting)); + } + + @After + public void tearDown() { + TestUtils.stopDatastore(); + } +} diff --git a/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/TestUtils.java b/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/TestUtils.java new file mode 100644 index 00000000000..0c4225ce96c --- /dev/null +++ b/appengine-java8/guestbook-cloud-datastore/src/test/java/com/example/guestbook/TestUtils.java @@ -0,0 +1,50 @@ +package com.example.guestbook; + +import static com.example.guestbook.Persistence.getDatastore; + +import com.google.cloud.datastore.Datastore; +import com.google.cloud.datastore.Key; +import com.google.cloud.datastore.Query; +import com.google.cloud.datastore.QueryResults; +import com.google.cloud.datastore.testing.LocalDatastoreHelper; +import com.google.common.collect.Lists; + +import org.threeten.bp.Duration; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.TimeoutException; + + +public class TestUtils { + static LocalDatastoreHelper datastore = LocalDatastoreHelper.create(); + + public static void startDatastore() { + try { + datastore.start(); + Persistence.setDatastore(datastore.getOptions().getService()); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public static void stopDatastore() { + try { + datastore.stop(Duration.ofSeconds(20 )); + Persistence.setDatastore(null); + } catch (TimeoutException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public static void wipeDatastore() { + Datastore datastore = getDatastore(); + QueryResults guestbooks = datastore.run(Query.newKeyQueryBuilder().setKind("Greeting") + .build()); + ArrayList keys = Lists.newArrayList(guestbooks); + + if (!keys.isEmpty()) { + datastore.delete(keys.toArray(new Key[keys.size()])); + } + } +} diff --git a/appengine-java8/guestbook-objectify/README.md b/appengine-java8/guestbook-objectify/README.md new file mode 100644 index 00000000000..2a7ee9cd289 --- /dev/null +++ b/appengine-java8/guestbook-objectify/README.md @@ -0,0 +1,17 @@ +# appengine/guestbook-objectify + +An App Engine guestbook using Java, Maven, and Objectify. + +Data access using [Objectify](https://github.com/objectify/objectify) + +Please ask questions on [Stackoverflow](http://stackoverflow.com/questions/tagged/google-app-engine) + +## Running Locally + +How do I, as a developer, start working on the project? + +1. `mvn clean appengine:devserver` + +## Deploying + +1. `mvn clean appengine:update -Dappengine.appId=PROJECT -Dappengine.version=VERSION` diff --git a/appengine-java8/guestbook-objectify/pom.xml b/appengine-java8/guestbook-objectify/pom.xml new file mode 100644 index 00000000000..13052937a2d --- /dev/null +++ b/appengine-java8/guestbook-objectify/pom.xml @@ -0,0 +1,126 @@ + + + + + 4.0.0 + war + 1.0-SNAPSHOT + + com.example.appengine + appengine-guestbook-objectify-j8 + + 5.1.17 + 20.0 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + jstl + jstl + 1.2 + + + + + com.google.guava + guava + ${guava.version} + + + com.googlecode.objectify + objectify + ${objectify.version} + + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.32 + test + + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Greeting.java b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Greeting.java new file mode 100644 index 00000000000..8067cd3fce7 --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Greeting.java @@ -0,0 +1,80 @@ +/** + * Copyright 2014-2015 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. + */ + +//[START all] +package com.example.guestbook; + +import com.googlecode.objectify.Key; +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; +import com.googlecode.objectify.annotation.Index; +import com.googlecode.objectify.annotation.Parent; + +import java.lang.String; +import java.util.Date; + +/** + * The @Entity tells Objectify about our entity. We also register it in {@link OfyHelper} + * Our primary key @Id is set automatically by the Google Datastore for us. + * + * We add a @Parent to tell the object about its ancestor. We are doing this to support many + * guestbooks. Objectify, unlike the AppEngine library requires that you specify the fields you + * want to index using @Index. Only indexing the fields you need can lead to substantial gains in + * performance -- though if not indexing your data from the start will require indexing it later. + * + * NOTE - all the properties are PUBLIC so that can keep the code simple. + **/ +@Entity +public class Greeting { + @Parent Key theBook; + @Id public Long id; + + public String authorEmail; + public String authorId; + public String content; + @Index public Date date; + + /** + * Simple constructor just sets the date. + **/ + public Greeting() { + date = new Date(); + } + + /** + * A convenience constructor. + **/ + public Greeting(String book, String content) { + this(); + if ( book != null ) { + theBook = Key.create(Guestbook.class, book); // Creating the Ancestor key + } else { + theBook = Key.create(Guestbook.class, "default"); + } + this.content = content; + } + + /** + * Takes all important fields. + **/ + public Greeting(String book, String content, String id, String email) { + this(book, content); + authorEmail = email; + authorId = id; + } + +} +//[END all] diff --git a/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Guestbook.java b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Guestbook.java new file mode 100644 index 00000000000..2b490aeff79 --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/Guestbook.java @@ -0,0 +1,33 @@ +/** + * Copyright 2014-2015 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. + */ + +//[START all] +package com.example.guestbook; + +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; + +/** + * The @Entity tells Objectify about our entity. We also register it in + * OfyHelper.java -- very important. + * + * This is never actually created, but gives a hint to Objectify about our Ancestor key. + */ +@Entity +public class Guestbook { + @Id public String book; +} +//[END all] diff --git a/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/OfyHelper.java b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/OfyHelper.java new file mode 100644 index 00000000000..3d34612007c --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/OfyHelper.java @@ -0,0 +1,37 @@ +/** + * Copyright 2014-2015 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. + */ +//[START all] +package com.example.guestbook; + +import com.googlecode.objectify.ObjectifyService; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +/** + * OfyHelper, a ServletContextListener, is setup in web.xml to run before a JSP is run. This is + * required to let JSP's access Ofy. + **/ +public class OfyHelper implements ServletContextListener { + public void contextInitialized(ServletContextEvent event) { + // This will be invoked as part of a warmup request, or the first user request if no warmup + // request. + ObjectifyService.register(Guestbook.class); + ObjectifyService.register(Greeting.class); + } + + public void contextDestroyed(ServletContextEvent event) { + // App Engine does not currently invoke this method. + } +} +//[END all] diff --git a/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/SignGuestbookServlet.java b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/SignGuestbookServlet.java new file mode 100644 index 00000000000..e32912cb933 --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/main/java/com/example/guestbook/SignGuestbookServlet.java @@ -0,0 +1,62 @@ +/** + * Copyright 2014-2015 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. + */ + +//[START all] +package com.example.guestbook; + +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; + +import com.googlecode.objectify.ObjectifyService; + +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Form Handling Servlet - most of the action for this sample is in webapp/guestbook.jsp, + * which displays the {@link Greeting}'s. + */ +public class SignGuestbookServlet extends HttpServlet { + + // Process the http POST of the form + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + Greeting greeting; + + UserService userService = UserServiceFactory.getUserService(); + User user = userService.getCurrentUser(); // Find out who the user is. + + String guestbookName = req.getParameter("guestbookName"); + String content = req.getParameter("content"); + if (user != null) { + greeting = new Greeting(guestbookName, content, user.getUserId(), user.getEmail()); + } else { + greeting = new Greeting(guestbookName, content); + } + + // Use Objectify to save the greeting and now() is used to make the call synchronously as we + // will immediately get a new page using redirect and we want the data to be present. + ObjectifyService.ofy().save().entity(greeting).now(); + + resp.sendRedirect("/guestbook.jsp?guestbookName=" + guestbookName); + } +} +//[END all] diff --git a/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..b85f72b69e3 --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,9 @@ + + + java8 + true + + + + + diff --git a/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..a17206681f0 --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,13 @@ +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# + +# Set the default logging level for all loggers to WARNING +.level = WARNING diff --git a/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/web.xml b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..d5a23626f0c --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,35 @@ + + + + + + sign + com.example.guestbook.SignGuestbookServlet + + + + sign + /sign + + + + guestbook.jsp + + + + + + ObjectifyFilter + com.googlecode.objectify.ObjectifyFilter + + + ObjectifyFilter + /* + + + com.example.guestbook.OfyHelper + + + diff --git a/appengine-java8/guestbook-objectify/src/main/webapp/guestbook.jsp b/appengine-java8/guestbook-objectify/src/main/webapp/guestbook.jsp new file mode 100644 index 00000000000..481274ceb84 --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/main/webapp/guestbook.jsp @@ -0,0 +1,106 @@ +<%-- //[START all]--%> +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="com.google.appengine.api.users.User" %> +<%@ page import="com.google.appengine.api.users.UserService" %> +<%@ page import="com.google.appengine.api.users.UserServiceFactory" %> + +<%-- //[START imports]--%> +<%@ page import="com.example.guestbook.Greeting" %> +<%@ page import="com.example.guestbook.Guestbook" %> +<%@ page import="com.googlecode.objectify.Key" %> +<%@ page import="com.googlecode.objectify.ObjectifyService" %> +<%-- //[END imports]--%> + +<%@ page import="java.util.List" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + + + + + + + + +<% + String guestbookName = request.getParameter("guestbookName"); + if (guestbookName == null) { + guestbookName = "default"; + } + pageContext.setAttribute("guestbookName", guestbookName); + UserService userService = UserServiceFactory.getUserService(); + User user = userService.getCurrentUser(); + if (user != null) { + pageContext.setAttribute("user", user); +%> + +

Hello, ${fn:escapeXml(user.nickname)}! (You can + sign out.)

+<% + } else { +%> +

Hello! + Sign in + to include your name with greetings you post.

+<% + } +%> + +<%-- //[START datastore]--%> +<% + // Create the correct Ancestor key + Key theBook = Key.create(Guestbook.class, guestbookName); + + // Run an ancestor query to ensure we see the most up-to-date + // view of the Greetings belonging to the selected Guestbook. + List greetings = ObjectifyService.ofy() + .load() + .type(Greeting.class) // We want only Greetings + .ancestor(theBook) // Anyone in this book + .order("-date") // Most recent first - date is indexed. + .limit(5) // Only show 5 of them. + .list(); + + if (greetings.isEmpty()) { +%> +

Guestbook '${fn:escapeXml(guestbookName)}' has no messages.

+<% + } else { +%> +

Messages in Guestbook '${fn:escapeXml(guestbookName)}'.

+<% + // Look at all of our greetings + for (Greeting greeting : greetings) { + pageContext.setAttribute("greeting_content", greeting.content); + String author; + if (greeting.authorEmail == null) { + author = "An anonymous person"; + } else { + author = greeting.authorEmail; + String author_id = greeting.authorId; + if (user != null && user.getUserId().equals(author_id)) { + author += " (You)"; + } + } + pageContext.setAttribute("greeting_user", author); +%> +

${fn:escapeXml(greeting_user)} wrote:

+
${fn:escapeXml(greeting_content)}
+<% + } + } +%> + +
+
+
+ +
+<%-- //[END datastore]--%> +
+
+
+
+ + + +<%-- //[END all]--%> diff --git a/appengine-java8/guestbook-objectify/src/main/webapp/stylesheets/main.css b/appengine-java8/guestbook-objectify/src/main/webapp/stylesheets/main.css new file mode 100644 index 00000000000..05d72d5536d --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/main/webapp/stylesheets/main.css @@ -0,0 +1,4 @@ +body { + font-family: Verdana, Helvetica, sans-serif; + background-color: #FFFFCC; +} diff --git a/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GreetingTest.java b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GreetingTest.java new file mode 100644 index 00000000000..27d3030149d --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GreetingTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016 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.example.guestbook; + +import static com.example.guestbook.GuestbookTestUtilities.cleanDatastore; +import static org.junit.Assert.assertEquals; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import com.googlecode.objectify.ObjectifyService; +import com.googlecode.objectify.util.Closeable; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + + +@RunWith(JUnit4.class) +public class GreetingTest { + private static final String TEST_CONTENT = "The world is Blue today"; + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + private Closeable closeable; + private DatastoreService ds; + + @Before + public void setUp() throws Exception { + + helper.setUp(); + ds = DatastoreServiceFactory.getDatastoreService(); + + ObjectifyService.register(Guestbook.class); + ObjectifyService.register(Greeting.class); + + closeable = ObjectifyService.begin(); + + cleanDatastore(ds, "default"); + } + + @After + public void tearDown() { + cleanDatastore(ds, "default"); + helper.tearDown(); + closeable.close(); + } + + @Test + public void createSaveObject() throws Exception { + + Greeting g = new Greeting("default", TEST_CONTENT); + ObjectifyService.ofy().save().entity(g).now(); + + Query query = new Query("Greeting") + .setAncestor(new KeyFactory.Builder("Guestbook", "default").getKey()); + PreparedQuery pq = ds.prepare(query); + Entity greeting = pq.asSingleEntity(); // Should only be one at this point. + assertEquals(greeting.getProperty("content"), TEST_CONTENT); + } +} diff --git a/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GuestbookTestUtilities.java b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GuestbookTestUtilities.java new file mode 100644 index 00000000000..cbd72b57b28 --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/GuestbookTestUtilities.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 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.example.guestbook; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; + +import java.util.ArrayList; +import java.util.List; + +public class GuestbookTestUtilities { + + public static void cleanDatastore(DatastoreService ds, String book) { + Query query = new Query("Greeting") + .setAncestor(new KeyFactory.Builder("Guestbook", book) + .getKey()).setKeysOnly(); + PreparedQuery pq = ds.prepare(query); + List entities = pq.asList(FetchOptions.Builder.withDefaults()); + ArrayList keys = new ArrayList<>(entities.size()); + + for (Entity e : entities) { + keys.add(e.getKey()); + } + ds.delete(keys); + } + +} diff --git a/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/SignGuestbookServletTest.java b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/SignGuestbookServletTest.java new file mode 100644 index 00000000000..1c01ab37f8d --- /dev/null +++ b/appengine-java8/guestbook-objectify/src/test/java/com/example/guestbook/SignGuestbookServletTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2015 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.example.guestbook; + +import static com.example.guestbook.GuestbookTestUtilities.cleanDatastore; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import com.googlecode.objectify.ObjectifyService; +import com.googlecode.objectify.util.Closeable; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link com.example.guestbook.SignGuestbookServlet}. + */ +@RunWith(JUnit4.class) +public class SignGuestbookServletTest { + private static final String FAKE_URL = "fakey.org/sign"; + private static final String FAKE_NAME = "Fake"; + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + private final String testPhrase = "Noew is the time"; + + @Mock private HttpServletRequest mockRequest; + + @Mock + private HttpServletResponse mockResponse; + + private StringWriter stringWriter; + private SignGuestbookServlet servletUnderTest; + private Closeable closeable; + private DatastoreService ds; + + @Before + public void setUp() throws Exception { + + MockitoAnnotations.initMocks(this); + helper.setUp(); + ds = DatastoreServiceFactory.getDatastoreService(); + + // Set up some fake HTTP requests + when(mockRequest.getRequestURI()).thenReturn(FAKE_URL); + when(mockRequest.getParameter("guestbookName")).thenReturn( "default" ); + when(mockRequest.getParameter("content")).thenReturn( testPhrase ); + + stringWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(stringWriter)); + + servletUnderTest = new SignGuestbookServlet(); + + ObjectifyService.register(Guestbook.class); + ObjectifyService.register(Greeting.class); + + closeable = ObjectifyService.begin(); + + cleanDatastore(ds, "default"); + } + + @After public void tearDown() { + cleanDatastore(ds, "default"); + helper.tearDown(); + closeable.close(); + } + + @Test + public void doPost_userNotLoggedIn() throws Exception { + servletUnderTest.doPost(mockRequest, mockResponse); + + Query query = new Query("Greeting") + .setAncestor(new KeyFactory.Builder("Guestbook", "default").getKey()); + PreparedQuery pq = ds.prepare(query); + + Entity greeting = pq.asSingleEntity(); // Should only be one at this point. + assertEquals(greeting.getProperty("content"), testPhrase); + } + +} diff --git a/appengine-java8/helloworld/README.md b/appengine-java8/helloworld/README.md new file mode 100644 index 00000000000..204ac802e96 --- /dev/null +++ b/appengine-java8/helloworld/README.md @@ -0,0 +1,18 @@ +# Google App Engine Standard Environment Hello World Sample + +This sample demonstrates how to deploy an application on Google App Engine. + +See the [Google App Engine standard environment documentation][ae-docs] for more +detailed instructions. + +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Setup + + gcloud init + +## Running locally + $ mvn appengine:run + +## Deploying + $ mvn appengine:deploy diff --git a/appengine-java8/helloworld/jenkins.sh b/appengine-java8/helloworld/jenkins.sh new file mode 100644 index 00000000000..22dfb5b12f4 --- /dev/null +++ b/appengine-java8/helloworld/jenkins.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# Copyright 2017 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. + +set -xe + +mvn clean appengine:deploy -DskipTests=true + +curl -f "http://${GOOGLE_VERSION_ID}-dot-${GOOGLE_PROJECT_ID}.appspot.com/" diff --git a/appengine-java8/helloworld/pom.xml b/appengine-java8/helloworld/pom.xml new file mode 100644 index 00000000000..ad046383886 --- /dev/null +++ b/appengine-java8/helloworld/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-helloworld-j8 + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + + diff --git a/appengine-java8/helloworld/src/main/java/com/example/appengine/helloworld/HelloServlet.java b/appengine-java8/helloworld/src/main/java/com/example/appengine/helloworld/HelloServlet.java new file mode 100644 index 00000000000..5868bf82f9d --- /dev/null +++ b/appengine-java8/helloworld/src/main/java/com/example/appengine/helloworld/HelloServlet.java @@ -0,0 +1,36 @@ +/** + * Copyright 2015 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.example.appengine.helloworld; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class HelloServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + PrintWriter out = resp.getWriter(); + out.println("Hello, world"); + } +} +// [END example] diff --git a/appengine-java8/helloworld/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/helloworld/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..1f8086c81f4 --- /dev/null +++ b/appengine-java8/helloworld/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,19 @@ + + + + + java8 + true + + diff --git a/appengine-java8/helloworld/src/main/webapp/WEB-INF/web.xml b/appengine-java8/helloworld/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..1a1704104a2 --- /dev/null +++ b/appengine-java8/helloworld/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,14 @@ + + + + hello + com.example.appengine.helloworld.HelloServlet + + + hello + / + + diff --git a/appengine-java8/images/README.md b/appengine-java8/images/README.md new file mode 100644 index 00000000000..88edaa825f2 --- /dev/null +++ b/appengine-java8/images/README.md @@ -0,0 +1,36 @@ +# Google App Engine Standard Environment Images Sample + +This sample demonstrates how to use the Images Java API. + +See the [Google App Engine standard environment documentation][ae-docs] for more +detailed instructions. + +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Modify the app + +Using the [Google Cloud SDK](https://cloud.google.com/sdk/) create a bucket + + $ gsutil mb YOUR-PROJECT-ID.appspot.com + +* Edit `src/main/java/com/example/appengine/images/ImageServlet.java` and set your `bucket` name. + +## Running locally + + This example uses the + [App Engine maven plugin](https://cloud.google.com/appengine/docs/java/tools/maven). + To run this sample locally: + + $ mvn appengine:devserver + + To see the results of the sample application, open + [localhost:8080](http://localhost:8080) in a web browser. + + +## Deploying + + In the following command, replace YOUR-PROJECT-ID with your + [Google Cloud Project ID](https://developers.google.com/console/help/new/#projectnumber) + and SOME-VERSION with a valid version number. + + $ mvn appengine:update -Dappengine.appId=YOUR-PROJECT-ID -Dappengine.version=SOME-VERSION diff --git a/appengine-java8/images/pom.xml b/appengine-java8/images/pom.xml new file mode 100644 index 00000000000..c3cd74df480 --- /dev/null +++ b/appengine-java8/images/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-images-j8 + + + 1.9.52 + + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + com.google.appengine.tools + appengine-gcs-client + 0.6 + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/images/src/main/java/com/example/appengine/images/ImagesServlet.java b/appengine-java8/images/src/main/java/com/example/appengine/images/ImagesServlet.java new file mode 100644 index 00000000000..2f0c1f9864a --- /dev/null +++ b/appengine-java8/images/src/main/java/com/example/appengine/images/ImagesServlet.java @@ -0,0 +1,118 @@ +/** + * Copyright 2015 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.example.appengine.images; +import com.google.appengine.api.blobstore.BlobKey; +import com.google.appengine.api.blobstore.BlobstoreService; +import com.google.appengine.api.blobstore.BlobstoreServiceFactory; +import com.google.appengine.api.images.Image; +import com.google.appengine.api.images.ImagesService; +import com.google.appengine.api.images.ImagesServiceFactory; +import com.google.appengine.api.images.Transform; +import com.google.appengine.tools.cloudstorage.GcsFileOptions; +import com.google.appengine.tools.cloudstorage.GcsFilename; +import com.google.appengine.tools.cloudstorage.GcsService; +import com.google.appengine.tools.cloudstorage.GcsServiceFactory; +import com.google.appengine.tools.cloudstorage.RetryParams; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class ImagesServlet extends HttpServlet { + final String bucket = "YOUR-BUCKETNAME-HERE"; + + // [START gcs] + private final GcsService gcsService = GcsServiceFactory.createGcsService(new RetryParams.Builder() + .initialRetryDelayMillis(10) + .retryMaxAttempts(10) + .totalRetryPeriodMillis(15000) + .build()); + // [END gcs] + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + //[START original_image] + // Read the image.jpg resource into a ByteBuffer. + FileInputStream fileInputStream = new FileInputStream(new File("WEB-INF/image.jpg")); + FileChannel fileChannel = fileInputStream.getChannel(); + ByteBuffer byteBuffer = ByteBuffer.allocate((int)fileChannel.size()); + fileChannel.read(byteBuffer); + + byte[] imageBytes = byteBuffer.array(); + + // Write the original image to Cloud Storage + gcsService.createOrReplace( + new GcsFilename(bucket, "image.jpeg"), + new GcsFileOptions.Builder().mimeType("image/jpeg").build(), + ByteBuffer.wrap(imageBytes)); + //[END original_image] + + //[START resize] + // Get an instance of the imagesService we can use to transform images. + ImagesService imagesService = ImagesServiceFactory.getImagesService(); + + // Make an image directly from a byte array, and transform it. + Image image = ImagesServiceFactory.makeImage(imageBytes); + Transform resize = ImagesServiceFactory.makeResize(100, 50); + Image resizedImage = imagesService.applyTransform(resize, image); + + // Write the transformed image back to a Cloud Storage object. + gcsService.createOrReplace( + new GcsFilename(bucket, "resizedImage.jpeg"), + new GcsFileOptions.Builder().mimeType("image/jpeg").build(), + ByteBuffer.wrap(resizedImage.getImageData())); + //[END resize] + + //[START rotate] + // Make an image from a Cloud Storage object, and transform it. + BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService(); + BlobKey blobKey = blobstoreService.createGsBlobKey("/gs/" + bucket + "/image.jpeg"); + Image blobImage = ImagesServiceFactory.makeImageFromBlob(blobKey); + Transform rotate = ImagesServiceFactory.makeRotate(90); + Image rotatedImage = imagesService.applyTransform(rotate, blobImage); + + // Write the transformed image back to a Cloud Storage object. + gcsService.createOrReplace( + new GcsFilename(bucket, "rotatedImage.jpeg"), + new GcsFileOptions.Builder().mimeType("image/jpeg").build(), + ByteBuffer.wrap(rotatedImage.getImageData())); + //[END rotate] + + // Output some simple HTML to display the images we wrote to Cloud Storage + // in the browser. + PrintWriter out = resp.getWriter(); + out.println("\n"); + out.println("AppEngine logo"); + out.println("AppEngine logo resized"); + out.println("AppEngine logo rotated"); + out.println("\n"); + } +} +// [END example] diff --git a/appengine-java8/images/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/images/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..d64ab6aafb7 --- /dev/null +++ b/appengine-java8/images/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,19 @@ + + + + + + java8 + true + diff --git a/appengine-java8/images/src/main/webapp/WEB-INF/image.jpg b/appengine-java8/images/src/main/webapp/WEB-INF/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3a60da2619de76a5461d91e40103e835f45c8eeb GIT binary patch literal 7933 zcmaiYcQoAH_wJ0*6NDgoi4kR#kPw|A$_#=Top>Wc2!qi=5MhWU!Y~XWTJ+v~ix9mf zMjeBQ7QJ`jdhhRkZ~5b{-~OEQInP>qpJ$yv&fe!)`*Py)7vKh53#J7C0)c=hS0CVV z4#2AE>*N3c=<5SS0RRB?)x5IHDS*bmQU9%xkpY2M14#BC`#*^6Uy}ce;QtBxPxybk zUbX@lsR3?)7!dF_fQ%6cVgz1(2k-!Z05USr75jgk_v%kgLq$tLd5xUxU%&hf0FWFA z0+CUG$jB)uuG~N}5IF@UfQpftiT|b)4fAcN5sS2~0Gd`k_zUYTgvU1cUd*?-tF7F? ztF2ee|C<;908vm<0m-PzX|CJ~i~us=6)A{L%2d`L+={ zm^$W5`L=+x`qOVe=UDC`JutYeOCsPp=<3vrAVz>HV0K>r{!h*O1bt?t#>w&}Ks7~1 ziQ4f zG!cK9vtL}(xCC6=Ui#9}uoxAb!)(>J`^{2oMblU-W-^MVEu*}ZJo(A>B$qK6E7Z+L z^&?vfknerh z;`yY!MejK~gn95Htn8M#o{w6F81*Q`iPwc2hI3k)PQ8c!s1g>Tg|{M%@eH&u7&+r6 zebbT8%0&HnQzq*mFW&nSz%3=Jccu%&36b>bM3I~3u$rXR1$d2whye|~TyCI)dShdC zRc@Ph;acEeYtiYG8AZ&4*Rr(Cj_A#tdg8_MhIM!{o2wJcqr8B{k1zH(8VI$xM0aJA zZ5uwzfVPGCZJ+mU1x`o9FCrb)T6)81d4Ue^&Cs;<+a#%{*)f$NuWXYcT|al|Gtsta z7cq-)ldsLVUv{FCQ>C0AUtullx$hHzy{NX{v)K@<<20-D94>f5ehDD@b{!P>bZl`K zq)~QCpV@@-=hH>|QV|RVg|OvZ*!3U+a;em#Vxytgr>lTF^&g)FlwSOnd7f@q-Q?0O zzAOhTZx`g*{6Mi@R#^tkQ!9OiRMu#3Q~XOu5qL=Jf(KM(HfRWwZ&YRVhQcLDD3u@0 zn$JiU$FR?9gzSbbcQZg5vAk#DLkgSXFEw<#v!WcycXW!m%FBA25cG+R;jI|V(K#eZ z%zM9v_2`h`rSD=g8W;JFFY*##`GoLceiO95 zFS=xOyVF0&-Guwi>+hJsmRCDlj{=_F`=&&qsOG}(EN%R9Yp^BI_tY~7x3&6z0*2W)+XJ?JqSZrJ2tB02W z&iC9dAq@LpO&gGd`PMv&Axx!T9mndTH{%d~$odVu6&Wh71C{xhA(hk35oImcnNjg9*A6?*SWGQ3ywX~Qp?C3qkq49gtlbMs%g-z{ z6ps|y2QPl;Kn>Ty4KCd@T1-(N(_u zuZs3BkjY*?jwxb(RXr&e*!P>~Tivd9w^> z017RJ01aD55N0GCbTuA{yh9BPRwAy4`))u*o<&oQsKX90<7Rr75tMLxa+Xk7y6l)Fikt9z0CuO8is zf@|jy=$Rr!%8e^n-|d97H8RIt9Jt304Z{vUJ*M*l)lRw2%kw}fl0#POuNN5Sqp~~o zKMdn@?!nCXYEp-@1Dmn}AMO>#y$j{1QNI!YBQ%tsT8A1%qXS38MUkoL0Dy;Fbe89R z%LkpMGM)Hu!d`0>f;KyECGPC@!##g};r@iZu^u=$YUHsT>uC<9|I%r8XLGLgS-EMy z)N^tgQc%>$C#pXL7SM}U(v92 zn&tCeB|3PHUAYn4;dZ{Da;XBtwJijcanguao90)(MmH6fr0vET`EcESauu!{26K=$ zS$Gw}&kw&K>lQkOjz1pq=$3n{?1?iF^$`?%&N^kUDViwK#WW=zhF)A=k|S#Kye@U) zSb{C59?mV)B`(KAS)@eVlj`z$3Om@^D~YVF4H>2$qc&wRBooauF$An<0$xsmSl&Ij z1muaCJ>Bt}WZ73N!p$|DR2E8!J#EoUY3j7C%5D}soIt?}^4x;gbc3p^=zSq(!%ha$ z#}VFvsnC~@sdJxj*Sr6u8Ov=}!#g!yjMm>XEs$iZPM`f%-Y9$9_RT;j(vouO;pg}T z?w4qo3}_pwTY;)hEWcTf7$90EN%H+`LK&GkQJr#Y z8Q`QPqFUHmLz_OvC`nz-KJBry2v=CR?W_;e2upT;)}YTp#9TZbY49P^l?HF-j4AoQ825P@Bt^Fnyu0(G zz#4y@#oW$^Ry}^+o=T@l#F`HS!zI=h{oD|8WGdTcdmQDgC}WN)^sc&HHu zB8aP(n)@lD$USC1X8Z3`+&cxV-|5$XN>i)@9W|4d_mj-`!@h0JWQmPN9kNdV#ZDKLj#XZdFdy6o;uwfOG_WZ^scv9g7rE|wO$t)u?PoGJ}~x7fOs74rgYroX=Ll(93H2t(!)zrJmp@% zk_1Hdu_|Ok%jWO#?m$rYcEH~GiP8mq+WkdFE#BPOgdu_=LpwO=!^c&^{aQ+%41oyF z02m;4e^41xjC!m)DitNQM)2%>Xi6=PFcqjamd~fxj06=~S)`M~X%J6!;(MZpmvsHq1Rr8<+@JxOraU6u;`ezKnJ-dk-@&AY zcx;?lU^JhK@DK651k_so3NYi(IVzYg54(371F4y;wW~?@2jL{{lsy!MF z>usje`t2%NGkj1tT{U^T{v~65WFJ@zNrwsHq+*lrq@HB~Ep;}RZZA`*d};US;3**x zH8^$I;$Ruq<-@b=% z28T8b2ignNtBLTDsbix&$)7{E8^(7b@gLV-oAoUVjWF@;V-3gCOTY_XP%2>T+_Rn4T0Rd?kl^vB z@-f*_Thn4wrg%ep%-a#B${0f9JxAxWWm#@;z&5OS9HaZ`PVfqeyTL(58&K5dCQJXY} z^CrZ)5~!d1j0O77*B{=`kK}5O;bWPX05f-`OMn6fmC`tp{gz(p;NsvxgK)1+M2!6e zW{~A!b@6*NT{e&52(ioB0jdt06YQ?|IXyqro112v-yS+7hc!JzJ788iNqP?i+r<#E z#DoRY8i_U2^7;Djip+xG6W}ma{ixIar&`9QQ7Cz6J<=-yaEMURNmK1|=O`oLT_3b? z-RrPjw@AjPpo3^O# zrQ9bNDJ1-tt~tNDsC(8b(C8n(u2t0Za3W=zi?vbt`3^Z?)7G`NUqs<6^d-KR3-(^tuPH#Ml_V5 zx5*c=>6E20a5xkZ#}pT0Y^+8DBH;qVF$Ghr5x=W{Zt1!Xq&E#A>p8K^#Z?eA22axN*#Wn?eg#)%l1FoQCw3jdcJ73*!Wq|MfTJWomrzOFVW6c zlE66M)id)fQu!1=OzTSv^~*`exegvM zIkF~Jf7P;Kt3Axxt&6s0*b{Zp*=l5*s@9{=RS|18cH4Y;3o<2U$GEb`kyrRx*NAnP zE3kdgrEsy|;h%mRRT1%Vlb&n#g5p}JnZX0Hz0}h%(`NI7tM04_YbG`nkz`>=dDzcB%u$Rp3%|>1B(%iI_ z6?U=nacV+XqRh_+LVnq{a{H^vj~*#jPPDJrjdXuP8kJkHq7GwLNY8{l43bSsoz<6kib4M&tAf#rq?dUg3d*AO_LO_i_nlxND#D59+_~ zf~x#QwVmZLJM@NN zPEK*o$eZ40-7(-ikq(iv^oinzCIOG5G(KEpeyeoT2I3z?2`M## zRl*B83-xfNJWgoWojYonf;CDhyqzv2Ine%^zcn!}d+TGpY==$uWE3f3-7N(br?_(G z0hhM=XNU!`EiOfFPjoM&iicr*LsBlJf34OBr%4f(%(~rKAqQ{}j)(2mwL0alAnOb# zI?#PIC)K6s)P%r1#|H{-)RTPz`pWCOO?yDj0Ix<=$SrRVT1(q<9GkU5&bs^le(z@0`^C`LVA=BTdu$1KgV*4!>jC?>eXm=L>{b zJC(#m`Kg4|9gdE_ij?Mzo_d{>p#T=9vT?-M+1QyxaG<_1&D=K5F(#5=`RX?*hR}SQSPs$~H$gvV==x4%nTaC!QqHFreOg^{oYZ+DeJ@0ICNDF$L42f-t+c8n zEZXol6W7N=D>DlpPT3pmz5#tE>w0Q>CpOVu_cL%rvxUMp-fkwWiWYakj#z7Xy|wEV zUAckWoym(iZB+Rp)JSJD!U3c)1g1in`S$0gNGt8(nn!xC{zDoAjmW2L$jb5~-3|#U zkvEE2W8ushw2JXWlU_vDwv5Qq#;AmW&1dV^de@SU2s5NSiTkID#_~X0uK3X|u52h6 zeMsnH+d&Pn{!=^>A2VUql}0aov^AZo4)mE~Zw2^p4hKNm%iaHen2N|9kuVp=X>-f- z1<{hJm%!wDbD#3wbw z<=62BVBom;kk0H(qGi``KRZECiTD^dw{3jdip<5ZUo&oQtsoA?Xr{D8Qz2FE*$(U3(Z3hId9HIH!@6q??g1W6{JIv)C(Ycy1H;Hjbd>L+24pNxiAWTM?(sTdN98ZUkLR!blGs) zKxbs%>2I2G1+EvF>|MgrDz~c5ZgJ}*CNc1Z=kuWFc@Kpu>UBST0>wS?-@F&u8R&eJ zljPLU?0r*VaixRX!mGIH^Y!;pW|PZo$u&|d-EsR5wR)W6r&Gul#bf{K+!+QxYas@! zPa$wsc|0=9#SM+1MsE29rn2V4$@Q-;r6LOwH*uwCXI69sh8^yFAm%TGWidHoaq~H< zT4Ts#E&{a$g9t8rhs*!!=-1xukL}{v!5UQjS$WNG)79rlW9^miN&?$52kadM{;IZ= z%oy!enl2iSi2WY^J?51H8`FMI>RjP7Y&Pm#zO{eHpfu_+-|RzTP1 zrk;8XtvVM37at9u@CAz%68-^jQsfLg2ztF6p1WSEueexOu^#G`dvUYXV}IU$UzWgN zVeViN;P`_t`>54>F~QPS)K4?#vx`rz7sp2QWYT-y2gs@^l!b8Vh&11m<9_Y4OTg-Z zc@ezvYpUOvP9%qzEP)4dUCW;9)TPU*hC}U~zXnI9%^RHG#rQYt$!IC3Vdbx^4?CIL z?fO20AMT}G(*)5dB_t2Jyn zw^e>>ol#)VqhUe&JL4N(O1h;@(Zf!K7|e@w1;6<+#D^8(V~*BV$gfsGp>v8hD_&@| zb?Sd)#Y_^Yx+~IHE&;nbGj{!>)u`4cWqW|Gv($X4)3d*s3jv;GynGPrUJl3g{^Cq5 zT|wPyEC+{x6#I>&kHg6tfqAynI#(^z)?a~jBNs(jo(3*QMng_4W8B?gCb>Ck1Mb$q z9zNXbMeCH8#hrha`SK(!*Egy)Gj)t!NpPoD)!%MjNImRSjg|T9xB-51<9k)p8wTL_ zH-Jb}3)3~!F8-FM&-44G#!s`F(NSZnfrh^gVUl+=U)bolRbj!oQxh+wC0YL*&?rnC zt*RV8qCeT%n_aW4j_k-6-N7|ufcP##?LpCIUzP}%aVT1`DI2D;VyHqb5Uo9nlj$>S ziHF}7*A!2Et*@)Hf%Po_pLkcDc`oVzZFEdCmFP2TYBW+qKX;h)P*tN*cJ;vh!f0xw zhd*5B>Jp(|`ZhM#?s3i)g+MlhDaa-*<8Tc8FyxcNJYIgaT@@mU5*bY|dscI$ zMAmf{AMp8GIo>~nyRkr*Zua&N4g8eF|{~rTf}NquX@z8&K^H^_!FHbeqJ< z-NHXo>tkU(baI5F8xyz#&SEiU5j{mf9jS}V*mt=qH?0thGT-B@4NniP_YIGhoBPrC z?R%7VxY3GxhUzM8??`7{gwty~vE;Vm%N^Y)VSZstZfAVncC`N#mxn?9_?Kq^bQPJ~ zESR|yRak*nhirtqM3&ru76flSG0|(?2yw#iF)d7%7AE9OOu}vH9&8;U3`4U7`M|(J ziiw#|U*-@`7ZckuCKo<3H(QwwM~sdP(KClnTPW!#16yCwrl$F4*t|;{AXVWT3bysetB`UVov*o^pJDc zt-NR=S5?9aB!1e=ng5@lmMT9J=0vvJSXJ!!(Be$ohdVwr9PDftysCRf_ex3%9Sx+>GLiK`Phg+yYF}Vb`I;f>>rAafp?P79gMwxgiO_GU9MWe>gEKm z{F@ukb&_}0Kg7(ej%3{*@kq&{VI-^dkEe zt*jt_QAI_z%KBvADTe8h#gZ zmSdzb)R^SQ+y3;^`e6zyaUJ;A&cr||j_{csKbRSf8Y2A5rWAJkx9^N6VGCKH~5^JAJLw;AIUR=_^I#x$L5&Y!Cy2h3r3)r4bFB7 z16pP1QvPt}u;5v@ovFWRr%p_|8WNfOmRI1#2E5oVj7%r`_98xMK+EJE9|2 z9_%mtC@K80=!O5um=*j+Xy_Lab%Y!o5gnKH8USh?U(>(c-|x|HewsYo`vF7yMLWGi Xju$)sSCzvlr`!tppN5~=<>dbWU!w^= literal 0 HcmV?d00001 diff --git a/appengine-java8/images/src/main/webapp/WEB-INF/web.xml b/appengine-java8/images/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..8ccba622877 --- /dev/null +++ b/appengine-java8/images/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,14 @@ + + + + images + com.example.appengine.images.ImagesServlet + + + images + / + + diff --git a/appengine-java8/logs/README.md b/appengine-java8/logs/README.md new file mode 100644 index 00000000000..ce1f661c796 --- /dev/null +++ b/appengine-java8/logs/README.md @@ -0,0 +1,18 @@ +# Users Authentication sample for Google App Engine + +This sample demonstrates how to use the [Logs API][log-docs] on [Google App +Engine][ae-docs]. + +[log-docs]: https://cloud.google.com/appengine/docs/java/logs/ +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Running locally + +The Logs API only generates output for deployed apps, so this program should not be run locally. + +## Deploying + +This example uses the +[Cloud SDK maven plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven). + + mvn appengine:deploy diff --git a/appengine-java8/logs/pom.xml b/appengine-java8/logs/pom.xml new file mode 100644 index 00000000000..3746dd6b2b2 --- /dev/null +++ b/appengine-java8/logs/pom.xml @@ -0,0 +1,76 @@ + + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-logs-j8 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + com.google.guava + guava + 20.0 + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + org.json + json + 20160810 + + + joda-time + joda-time + 2.9.9 + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/logs/src/main/java/com/example/appengine/logs/LogsServlet.java b/appengine-java8/logs/src/main/java/com/example/appengine/logs/LogsServlet.java new file mode 100644 index 00000000000..a889763e989 --- /dev/null +++ b/appengine-java8/logs/src/main/java/com/example/appengine/logs/LogsServlet.java @@ -0,0 +1,97 @@ +/* Copyright 2016 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. + */ +// [START logs_API_example] +package com.example.appengine.logs; + +import com.google.appengine.api.log.AppLogLine; +import com.google.appengine.api.log.LogQuery; +import com.google.appengine.api.log.LogServiceFactory; +import com.google.appengine.api.log.RequestLogs; + +import org.joda.time.DateTime; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +// Get request logs along with their app log lines and display them 5 at +// a time, using a Next link to cycle through to the next 5. +public class LogsServlet extends HttpServlet { + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + + resp.setContentType("text/html"); + PrintWriter writer = resp.getWriter(); + writer.println(""); + writer.println(""); + writer.println("App Engine Logs Sample"); + + // We use this to break out of our iteration loop, limiting record + // display to 5 request logs at a time. + int limit = 5; + + // This retrieves the offset from the Next link upon user click. + String offset = req.getParameter("offset"); + + // We want the App logs for each request log + LogQuery query = LogQuery.Builder.withDefaults(); + query.includeAppLogs(true); + + // Set the offset value retrieved from the Next link click. + if (offset != null) { + query.offset(offset); + } + + // This gets filled from the last request log in the iteration + String lastOffset = null; + int count = 0; + + // Display a few properties of each request log. + for (RequestLogs record : LogServiceFactory.getLogService().fetch(query)) { + writer.println("
REQUEST LOG
"); + DateTime reqTime = new DateTime(record.getStartTimeUsec() / 1000); + writer.println("IP: " + record.getIp() + "
"); + writer.println("Method: " + record.getMethod() + "
"); + writer.println("Resource " + record.getResource() + "
"); + writer.println(String.format("
Date: %s", reqTime.toString())); + + lastOffset = record.getOffset(); + + // Display all the app logs for each request log. + for (AppLogLine appLog : record.getAppLogLines()) { + writer.println("
" + "APPLICATION LOG" + "
"); + DateTime appTime = new DateTime(appLog.getTimeUsec() / 1000); + writer.println(String.format("
Date: %s", appTime.toString())); + writer.println("
Level: " + appLog.getLogLevel() + "
"); + writer.println("Message: " + appLog.getLogMessage() + "

"); + } + + if (++count >= limit) { + break; + } + } + + // When the user clicks this link, the offset is processed in the + // GET handler and used to cycle through to the next 5 request logs. + writer.println(String.format("
Next", lastOffset)); + } +} +// [END logs_API_example] + diff --git a/appengine-java8/logs/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/logs/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..202e048ae2e --- /dev/null +++ b/appengine-java8/logs/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,22 @@ + + + + + + java8 + true + + + + diff --git a/appengine-java8/logs/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/logs/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..3e7f85b9dc1 --- /dev/null +++ b/appengine-java8/logs/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,14 @@ +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# + +# Set the default logging level for all loggers to WARNING +.level = WARNING + diff --git a/appengine-java8/logs/src/main/webapp/WEB-INF/web.xml b/appengine-java8/logs/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..3b156c9b98c --- /dev/null +++ b/appengine-java8/logs/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,28 @@ + + + + + + + logs + com.example.appengine.logs.LogsServlet + + + logs + / + + diff --git a/appengine-java8/mail/README.md b/appengine-java8/mail/README.md new file mode 100644 index 00000000000..149eb8b485b --- /dev/null +++ b/appengine-java8/mail/README.md @@ -0,0 +1,21 @@ +# JavaMail API Email Sample for Google App Engine Standard Environment + +This sample demonstrates how to use [JavaMail][javamail-api] on [Google App Engine +standard environment][ae-docs]. + +See the [sample application documentaion][sample-docs] for more detailed +instructions. + +[ae-docs]: https://cloud.google.com/appengine/docs/java/ +[javamail-api]: http://javamail.java.net/ +[sample-docs]: https://cloud.google.com/appengine/docs/java/mail/ + +## Setup + + gcloud init + +## Running locally + $ mvn appengine:run + +## Deploying + $ mvn appengine:deploy diff --git a/appengine-java8/mail/pom.xml b/appengine-java8/mail/pom.xml new file mode 100644 index 00000000000..c696580c6d6 --- /dev/null +++ b/appengine-java8/mail/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-mail + + + + com.google.cloud + appengine-doc-samples + 1.0.0 + .. + + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + com.google.appengine + appengine-api-1.0-sdk + + + javax.mail + mail + 1.4.7 + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/mail/src/main/java/com/example/appengine/mail/BounceHandlerServlet.java b/appengine-java8/mail/src/main/java/com/example/appengine/mail/BounceHandlerServlet.java new file mode 100644 index 00000000000..51f4536ac8a --- /dev/null +++ b/appengine-java8/mail/src/main/java/com/example/appengine/mail/BounceHandlerServlet.java @@ -0,0 +1,54 @@ +/** + * Copyright 2016 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.example.appengine.mail; + +// [START bounce_handler_servlet] +import com.google.appengine.api.mail.BounceNotification; +import com.google.appengine.api.mail.BounceNotificationParser; + +import java.io.IOException; +import java.util.logging.Logger; +import javax.mail.MessagingException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class BounceHandlerServlet extends HttpServlet { + + private static final Logger log = Logger.getLogger(BounceHandlerServlet.class.getName()); + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + try { + BounceNotification bounce = BounceNotificationParser.parse(req); + log.warning("Bounced email notification."); + // The following data is available in a BounceNotification object + // bounce.getOriginal().getFrom() + // bounce.getOriginal().getTo() + // bounce.getOriginal().getSubject() + // bounce.getOriginal().getText() + // bounce.getNotification().getFrom() + // bounce.getNotification().getTo() + // bounce.getNotification().getSubject() + // bounce.getNotification().getText() + // ... + } catch (MessagingException e) { + // ... + } + } +} +// [END bounce_handler_servlet] diff --git a/appengine-java8/mail/src/main/java/com/example/appengine/mail/HandleDiscussionEmail.java b/appengine-java8/mail/src/main/java/com/example/appengine/mail/HandleDiscussionEmail.java new file mode 100644 index 00000000000..b673497f389 --- /dev/null +++ b/appengine-java8/mail/src/main/java/com/example/appengine/mail/HandleDiscussionEmail.java @@ -0,0 +1,43 @@ +/** + * Copyright 2016 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.example.appengine.mail; + +import javax.mail.internet.MimeMessage; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletException; +import java.util.logging.Logger; +import java.util.regex.Matcher; + +// [START example] +public class HandleDiscussionEmail extends MailHandlerBase { + + private static final Logger log = Logger.getLogger(HandleDiscussionEmail.class.getName()); + public HandleDiscussionEmail() { super("discuss-(.*)@(.*)"); } + + @Override + protected boolean processMessage(HttpServletRequest req, HttpServletResponse res) + throws ServletException + { + log.info("Received e-mail sent to discuss list."); + MimeMessage msg = getMessageFromRequest(req); + Matcher match = getMatcherFromRequest(req); + // ... + return true; + } +} +// [END example] diff --git a/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerBase.java b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerBase.java new file mode 100644 index 00000000000..dd16582bb39 --- /dev/null +++ b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerBase.java @@ -0,0 +1,116 @@ +/** + * Copyright 2016 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.example.appengine.mail; + +import javax.mail.internet.MimeMessage; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Base class for handling the filtering of incoming emails in App Engine. + */ +// [START example] +public abstract class MailHandlerBase implements Filter { + + private Pattern pattern = null; + + protected MailHandlerBase(String pattern) { + if (pattern == null || pattern.trim().length() == 0) + { + throw new IllegalArgumentException("Expected non-empty regular expression"); + } + this.pattern = Pattern.compile("/_ah/mail/"+pattern); + } + + @Override public void init(FilterConfig config) throws ServletException { } + + @Override public void destroy() { } + + /** + * Process the message. A message will only be passed to this method + * if the servletPath of the message (typically the recipient for + * appengine) satisfies the pattern passed to the constructor. If + * the implementation returns false, control is passed + * to the next filter in the chain. If the implementation returns + * true, the filter chain is terminated. + * + * The Matcher for the pattern can be retrieved via + * getMatcherFromRequest (e.g. if groups are used in the pattern). + */ + protected abstract boolean processMessage(HttpServletRequest req, HttpServletResponse res) throws ServletException; + + @Override + public void doFilter(ServletRequest sreq, ServletResponse sres, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest req = (HttpServletRequest) sreq; + HttpServletResponse res = (HttpServletResponse) sres; + + MimeMessage message = getMessageFromRequest(req); + Matcher m = applyPattern(req); + + if (m != null && processMessage(req, res)) { + return; + } + + chain.doFilter(req, res); // Try the next one + + } + + private Matcher applyPattern(HttpServletRequest req) { + Matcher m = pattern.matcher(req.getServletPath()); + if (!m.matches()) m = null; + + req.setAttribute("matcher", m); + return m; + } + + protected Matcher getMatcherFromRequest(ServletRequest req) { + return (Matcher) req.getAttribute("matcher"); + } + + protected MimeMessage getMessageFromRequest(ServletRequest req) throws ServletException { + MimeMessage message = (MimeMessage) req.getAttribute("mimeMessage"); + if (message == null) { + try { + Properties props = new Properties(); + Session session = Session.getDefaultInstance(props, null); + message = new MimeMessage(session, req.getInputStream()); + req.setAttribute("mimeMessage", message); + + } catch (MessagingException e) { + throw new ServletException("Error processing inbound message", e); + } catch (IOException e) { + throw new ServletException("Error processing inbound message", e); + } + } + return message; + } +} +// [END example] diff --git a/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerServlet.java b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerServlet.java new file mode 100644 index 00000000000..0262d41c95a --- /dev/null +++ b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailHandlerServlet.java @@ -0,0 +1,49 @@ +/** + * Copyright 2016 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.example.appengine.mail; + +// [START mail_handler_servlet] +import java.io.IOException; +import java.util.logging.Logger; +import java.util.Properties; + +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.internet.MimeMessage; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class MailHandlerServlet extends HttpServlet { + + private static final Logger log = Logger.getLogger(MailHandlerServlet.class.getName()); + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + Properties props = new Properties(); + Session session = Session.getDefaultInstance(props, null); + try { + MimeMessage message = new MimeMessage(session, req.getInputStream()); + log.info("Received mail message."); + } catch (MessagingException e) { + // ... + } + // ... + } +} +// [END mail_handler_servlet] diff --git a/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailServlet.java b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailServlet.java new file mode 100644 index 00000000000..a51fe854c6a --- /dev/null +++ b/appengine-java8/mail/src/main/java/com/example/appengine/mail/MailServlet.java @@ -0,0 +1,125 @@ +/** + * Copyright 2016 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.example.appengine.mail; + +// [START simple_includes] +import java.io.IOException; +import java.util.Properties; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +// [END simple_includes] + +// [START multipart_includes] +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; +import javax.activation.DataHandler; +import javax.mail.Multipart; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMultipart; +// [END multipart_includes] + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@SuppressWarnings("serial") +public class MailServlet extends HttpServlet { + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String type = req.getParameter("type"); + if (type != null && type.equals("multipart")) { + resp.getWriter().print("Sending HTML email with attachment."); + sendMultipartMail(); + } else { + resp.getWriter().print("Sending simple email."); + sendSimpleMail(); + } + } + + private void sendSimpleMail() { + // [START simple_example] + Properties props = new Properties(); + Session session = Session.getDefaultInstance(props, null); + + try { + Message msg = new MimeMessage(session); + msg.setFrom(new InternetAddress("admin@example.com", "Example.com Admin")); + msg.addRecipient(Message.RecipientType.TO, + new InternetAddress("user@example.com", "Mr. User")); + msg.setSubject("Your Example.com account has been activated"); + msg.setText("This is a test"); + Transport.send(msg); + } catch (AddressException e) { + // ... + } catch (MessagingException e) { + // ... + } catch (UnsupportedEncodingException e) { + // ... + } + // [END simple_example] + } + + private void sendMultipartMail() { + Properties props = new Properties(); + Session session = Session.getDefaultInstance(props, null); + + String msgBody = "..."; + + try { + Message msg = new MimeMessage(session); + msg.setFrom(new InternetAddress("admin@example.com", "Example.com Admin")); + msg.addRecipient(Message.RecipientType.TO, + new InternetAddress("user@example.com", "Mr. User")); + msg.setSubject("Your Example.com account has been activated"); + msg.setText(msgBody); + + // [START multipart_example] + String htmlBody = ""; // ... + byte[] attachmentData = null; // ... + Multipart mp = new MimeMultipart(); + + MimeBodyPart htmlPart = new MimeBodyPart(); + htmlPart.setContent(htmlBody, "text/html"); + mp.addBodyPart(htmlPart); + + MimeBodyPart attachment = new MimeBodyPart(); + InputStream attachmentDataStream = new ByteArrayInputStream(attachmentData); + attachment.setFileName("manual.pdf"); + attachment.setContent(attachmentDataStream, "application/pdf"); + mp.addBodyPart(attachment); + + msg.setContent(mp); + // [END multipart_example] + + Transport.send(msg); + + } catch (AddressException e) { + // ... + } catch (MessagingException e) { + // ... + } catch (UnsupportedEncodingException e) { + // ... + } + } +} diff --git a/appengine-java8/mail/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/mail/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..33b8f0522e6 --- /dev/null +++ b/appengine-java8/mail/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,28 @@ + + + + + + java8 + true + + + + + mail + + mail_bounce + + + diff --git a/appengine-java8/mail/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/mail/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..45b39a32093 --- /dev/null +++ b/appengine-java8/mail/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,27 @@ +# +# Copyright 2016 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. +# +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# +# Set the default logging level for all loggers to WARNING +.level=INFO diff --git a/appengine-java8/mail/src/main/webapp/WEB-INF/web.xml b/appengine-java8/mail/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..393f67066ad --- /dev/null +++ b/appengine-java8/mail/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,79 @@ + + + + + mail + com.example.appengine.mail.MailServlet + + + mail + / + + + + + HandleDiscussionEmail + com.example.appengine.mail.HandleDiscussionEmail + + + HandleDiscussionEmail + /_ah/mail/* + + + + + + + + + bouncehandler + com.example.appengine.mail.BounceHandlerServlet + + + bouncehandler + /_ah/bounce + + + + bounce + /_ah/bounce + + + admin + + + + diff --git a/appengine-java8/mailgun/README.md b/appengine-java8/mailgun/README.md new file mode 100644 index 00000000000..582771ddb34 --- /dev/null +++ b/appengine-java8/mailgun/README.md @@ -0,0 +1,11 @@ +# Java Mailgun Email Sample for Google App Engine Standard Environment + +This sample demonstrates how to use [Mailgun][mailgun-api] on [Google App Engine +standard environment][ae-docs]. + +See the [sample application documentaion][sample-docs] for more detailed +instructions. + +[ae-docs]: https://cloud.google.com/appengine/docs/java/ +[mailgun-api]: https://documentation.mailgun.com/ +[sample-docs]: https://cloud.google.com/appengine/docs/java/mail/mailgun diff --git a/appengine-java8/mailgun/pom.xml b/appengine-java8/mailgun/pom.xml new file mode 100644 index 00000000000..87576e1086f --- /dev/null +++ b/appengine-java8/mailgun/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-mailgun-j8 + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + com.sun.jersey + jersey-core + 1.19.3 + + + com.sun.jersey + jersey-client + 1.19.3 + + + com.sun.jersey.contribs + jersey-multipart + 1.19.3 + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/mailgun/src/main/java/com/example/appengine/mailgun/MailgunServlet.java b/appengine-java8/mailgun/src/main/java/com/example/appengine/mailgun/MailgunServlet.java new file mode 100644 index 00000000000..ad30b5e983a --- /dev/null +++ b/appengine-java8/mailgun/src/main/java/com/example/appengine/mailgun/MailgunServlet.java @@ -0,0 +1,92 @@ +/** + * Copyright 2015 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.example.appengine.mailgun; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter; +import com.sun.jersey.core.util.MultivaluedMapImpl; +import com.sun.jersey.multipart.FormDataMultiPart; +import com.sun.jersey.multipart.file.FileDataBodyPart; + +import java.io.File; +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; + +// [START example] +@SuppressWarnings("serial") +public class MailgunServlet extends HttpServlet { + + private static final String MAILGUN_DOMAIN_NAME = System.getenv("MAILGUN_DOMAIN_NAME"); + private static final String MAILGUN_API_KEY = System.getenv("MAILGUN_API_KEY"); + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String type = req.getParameter("submit"); + String recipient = req.getParameter("to"); + ClientResponse clientResponse; + if (type.equals("Send simple email")) { + clientResponse = sendSimpleMessage(recipient); + } else { + clientResponse = sendComplexMessage(recipient); + } + if (clientResponse.getStatus() == 200) { + resp.getWriter().print("Email sent."); + } + } + + // [START simple] + private ClientResponse sendSimpleMessage(String recipient) { + Client client = Client.create(); + client.addFilter(new HTTPBasicAuthFilter("api", MAILGUN_API_KEY)); + WebResource webResource = client.resource("https://api.mailgun.net/v3/" + MAILGUN_DOMAIN_NAME + + "/messages"); + MultivaluedMapImpl formData = new MultivaluedMapImpl(); + formData.add("from", "Mailgun User "); + formData.add("to", recipient); + formData.add("subject", "Simple Mailgun Example"); + formData.add("text", "Plaintext content"); + return webResource.type(MediaType.APPLICATION_FORM_URLENCODED).post(ClientResponse.class, + formData); + } + // [END simple] + + // [START complex] + private ClientResponse sendComplexMessage(String recipient) { + Client client = Client.create(); + client.addFilter(new HTTPBasicAuthFilter("api", MAILGUN_API_KEY)); + WebResource webResource = client.resource("https://api.mailgun.net/v3/" + MAILGUN_DOMAIN_NAME + + "/messages"); + FormDataMultiPart formData = new FormDataMultiPart(); + formData.field("from", "Mailgun User "); + formData.field("to", recipient); + formData.field("subject", "Complex Mailgun Example"); + formData.field("html", "HTML content"); + ClassLoader classLoader = getClass().getClassLoader(); + File txtFile = new File(classLoader.getResource("example-attachment.txt").getFile()); + formData.bodyPart(new FileDataBodyPart("attachment", txtFile, MediaType.TEXT_PLAIN_TYPE)); + return webResource.type(MediaType.MULTIPART_FORM_DATA_TYPE) + .post(ClientResponse.class, formData); + } + // [END complex] +} +// [END example] diff --git a/appengine-java8/mailgun/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/mailgun/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..dd000a989ec --- /dev/null +++ b/appengine-java8/mailgun/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,24 @@ + + + + + + java8 + true + + + + + + diff --git a/appengine-java8/mailgun/src/main/webapp/WEB-INF/web.xml b/appengine-java8/mailgun/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..9f22b168016 --- /dev/null +++ b/appengine-java8/mailgun/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,29 @@ + + + + + + + mailgun + com.example.appengine.mailgun.MailgunServlet + + + mailgun + /send/email + + + diff --git a/appengine-java8/mailgun/src/main/webapp/index.html b/appengine-java8/mailgun/src/main/webapp/index.html new file mode 100644 index 00000000000..9ec44eaad7c --- /dev/null +++ b/appengine-java8/mailgun/src/main/webapp/index.html @@ -0,0 +1,27 @@ + + + + + Mailgun on Google App Engine Managed VMs + + + +
+ + + +
+ + + diff --git a/appengine-java8/mailjet/README.md b/appengine-java8/mailjet/README.md new file mode 100644 index 00000000000..0a9bddaaac7 --- /dev/null +++ b/appengine-java8/mailjet/README.md @@ -0,0 +1,15 @@ +# Mailjet sample for Google App Engine +This sample demonstrates how to use [Mailjet](https://www.mailjet.com/) on Google Managed VMs to +send emails from a verified sender you own. + +## Setup +1. Before using, ensure the address you plan to send from has been verified in Mailjet. + +## Running locally + $ export MAILJET_API_KEY=[your mailjet api key] + $ export MAILJET_SECRET_KEY=[your mailjet secret key] + $ mvn clean appengine:devserver + +## Deploying +1. Edit the environment variables in the appengine-web.xml with the appropriate Mailjet values. + $ mvn clean appengine:update diff --git a/appengine-java8/mailjet/pom.xml b/appengine-java8/mailjet/pom.xml new file mode 100644 index 00000000000..a758834fdb8 --- /dev/null +++ b/appengine-java8/mailjet/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-mailjet-j8 + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + 4.0.5 + + + + com.mailjet + mailjet-client + ${mailjet.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + com.sun.jersey + jersey-core + 1.19.3 + + + com.sun.jersey + jersey-client + 1.19.3 + + + com.sun.jersey.contribs + jersey-multipart + 1.19.3 + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/mailjet/src/main/java/com/example/appengine/mailjet/MailjetServlet.java b/appengine-java8/mailjet/src/main/java/com/example/appengine/mailjet/MailjetServlet.java new file mode 100644 index 00000000000..e9922a71438 --- /dev/null +++ b/appengine-java8/mailjet/src/main/java/com/example/appengine/mailjet/MailjetServlet.java @@ -0,0 +1,73 @@ +/** + * Copyright 2016 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. + */ +// [START mailjet_imports] +package com.example.appengine.mailjet; + +import com.mailjet.client.MailjetClient; +import com.mailjet.client.MailjetRequest; +import com.mailjet.client.MailjetResponse; +import com.mailjet.client.errors.MailjetException; +import com.mailjet.client.errors.MailjetSocketTimeoutException; +import com.mailjet.client.resource.Email; +// [END mailjet_imports] + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START app] +@SuppressWarnings("serial") +public class MailjetServlet extends HttpServlet { + private static final String MAILJET_API_KEY = System.getenv("MAILJET_API_KEY"); + private static final String MAILJET_SECRET_KEY = System.getenv("MAILJET_SECRET_KEY"); + private MailjetClient client = new MailjetClient(MAILJET_API_KEY, MAILJET_SECRET_KEY); + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + String recipient = req.getParameter("to"); + String sender = req.getParameter("from"); + + MailjetRequest email = new MailjetRequest(Email.resource) + .property(Email.FROMEMAIL, sender) + .property(Email.FROMNAME, "pandora") + .property(Email.SUBJECT, "Your email flight plan!") + .property(Email.TEXTPART, + "Dear passenger, welcome to Mailjet! May the delivery force be with you!") + .property(Email.HTMLPART, + "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!") + .property(Email.RECIPIENTS, new JSONArray().put(new JSONObject().put("Email", recipient))); + + try { + // trigger the API call + MailjetResponse response = client.post(email); + // Read the response data and status + resp.getWriter().print(response.getStatus()); + resp.getWriter().print(response.getData()); + } catch (MailjetException e) { + throw new ServletException("Mailjet Exception", e); + } catch (MailjetSocketTimeoutException e) { + throw new ServletException("Mailjet socket timed out", e); + } + } +} +// [END app] diff --git a/appengine-java8/mailjet/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/mailjet/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..2be13ecdab1 --- /dev/null +++ b/appengine-java8/mailjet/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,25 @@ + + + + + + java8 + true +// [START env_variables] + + + + +// [END env_variables] + diff --git a/appengine-java8/mailjet/src/main/webapp/WEB-INF/web.xml b/appengine-java8/mailjet/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..9d906fce8f8 --- /dev/null +++ b/appengine-java8/mailjet/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,28 @@ + + + + + + + mailjet + com.example.appengine.mailjet.MailjetServlet + + + mailjet + /send/email + + diff --git a/appengine-java8/mailjet/src/main/webapp/index.html b/appengine-java8/mailjet/src/main/webapp/index.html new file mode 100644 index 00000000000..10f5dcc59dd --- /dev/null +++ b/appengine-java8/mailjet/src/main/webapp/index.html @@ -0,0 +1,27 @@ + + + + + Mailgun on Google App Engine Managed VMs + + + +
+ + + +
+ + + diff --git a/appengine-java8/memcache/pom.xml b/appengine-java8/memcache/pom.xml new file mode 100644 index 00000000000..e9c5c27dc8e --- /dev/null +++ b/appengine-java8/memcache/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-memcache-j8 + + + appengine-java8-samples + com.google.cloud + 1.0.0 + .. + + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + com.googlecode.xmemcached + xmemcached + 2.3.1 + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheAsyncCacheServlet.java b/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheAsyncCacheServlet.java new file mode 100644 index 00000000000..d1960e06cb5 --- /dev/null +++ b/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheAsyncCacheServlet.java @@ -0,0 +1,75 @@ +/** + * Copyright 2016 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.example.appengine.memcache; + +import com.google.appengine.api.memcache.AsyncMemcacheService; +import com.google.appengine.api.memcache.ErrorHandlers; +import com.google.appengine.api.memcache.MemcacheServiceFactory; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.logging.Level; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@SuppressWarnings("serial") +public class MemcacheAsyncCacheServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + String path = req.getRequestURI(); + if (path.startsWith("/favicon.ico")) { + return; // ignore the request for favicon.ico + } + + // [START example] + AsyncMemcacheService asyncCache = MemcacheServiceFactory.getAsyncMemcacheService(); + asyncCache.setErrorHandler(ErrorHandlers.getConsistentLogAndContinue(Level.INFO)); + String key = "count-async"; + byte[] value; + long count = 1; + Future futureValue = asyncCache.get(key); // Read from cache. + // ... Do other work in parallel to cache retrieval. + try { + value = (byte[]) futureValue.get(); + if (value == null) { + value = BigInteger.valueOf(count).toByteArray(); + asyncCache.put(key, value); + } else { + // Increment value + count = new BigInteger(value).longValue(); + count++; + value = BigInteger.valueOf(count).toByteArray(); + // Put back in cache + asyncCache.put(key, value); + } + } catch (InterruptedException | ExecutionException e) { + throw new ServletException("Error when waiting for future value", e); + } + // [END example] + + // Output content + resp.setContentType("text/plain"); + resp.getWriter().print("Value is " + count + "\n"); + } +} diff --git a/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheBestPracticeServlet.java b/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheBestPracticeServlet.java new file mode 100644 index 00000000000..b4bd8cc9cc9 --- /dev/null +++ b/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheBestPracticeServlet.java @@ -0,0 +1,57 @@ +/** + * Copyright 2016 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.example.appengine.memcache; + +import com.google.appengine.api.memcache.ErrorHandlers; +import com.google.appengine.api.memcache.MemcacheService; +import com.google.appengine.api.memcache.MemcacheServiceFactory; + +import java.io.IOException; +import java.util.logging.Level; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class MemcacheBestPracticeServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + String path = req.getRequestURI(); + if (path.startsWith("/favicon.ico")) { + return; // ignore the request for favicon.ico + } + + MemcacheService syncCache = MemcacheServiceFactory.getMemcacheService(); + syncCache.setErrorHandler(ErrorHandlers.getConsistentLogAndContinue(Level.INFO)); + + byte[] whoKey = "who".getBytes(); + byte[] countKey = "count".getBytes(); + + byte[] who = (byte[]) syncCache.get(whoKey); + String whoString = who == null ? "nobody" : new String(who); + resp.getWriter().print("Previously incremented by " + whoString + "\n"); + syncCache.put(whoKey, "Java".getBytes()); + Long count = syncCache.increment(countKey, 1L, 0L); + resp.getWriter().print("Count incremented by Java = " + count + "\n"); + } +} +// [END example] diff --git a/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheConcurrentServlet.java b/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheConcurrentServlet.java new file mode 100644 index 00000000000..212c5d990a8 --- /dev/null +++ b/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheConcurrentServlet.java @@ -0,0 +1,85 @@ +/** + * Copyright 2016 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.example.appengine.memcache; + +import com.google.appengine.api.memcache.MemcacheService; +import com.google.appengine.api.memcache.MemcacheService.IdentifiableValue; +import com.google.appengine.api.memcache.MemcacheServiceFactory; + +import java.io.IOException; +import java.math.BigInteger; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class MemcacheConcurrentServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + String path = req.getRequestURI(); + if (path.startsWith("/favicon.ico")) { + return; // ignore the request for favicon.ico + } + + String key = "count-concurrent"; + // Using the synchronous cache. + MemcacheService syncCache = MemcacheServiceFactory.getMemcacheService(); + + // Write this value to cache using getIdentifiable and putIfUntouched. + for (long delayMs = 1; delayMs < 1000; delayMs *= 2) { + IdentifiableValue oldValue = syncCache.getIdentifiable(key); + byte[] newValue = oldValue == null + ? BigInteger.valueOf(0).toByteArray() + : increment((byte[]) oldValue.getValue()); // newValue depends on old value + resp.setContentType("text/plain"); + resp.getWriter().print("Value is " + new BigInteger(newValue).intValue() + "\n"); + if (oldValue == null) { + // Key doesn't exist. We can safely put it in cache. + syncCache.put(key, newValue); + break; + } else if (syncCache.putIfUntouched(key, oldValue, newValue)) { + // newValue has been successfully put into cache. + break; + } else { + // Some other client changed the value since oldValue was retrieved. + // Wait a while before trying again, waiting longer on successive loops. + try { + Thread.sleep(delayMs); + } catch (InterruptedException e) { + throw new ServletException("Error when sleeping", e); + } + } + } + } + + /** + * Increments an integer stored as a byte array by one. + * @param oldValue a byte array with the old value + * @return a byte array as the old value increased by one + */ + private byte[] increment(byte[] oldValue) { + long val = new BigInteger(oldValue).intValue(); + val++; + return BigInteger.valueOf(val).toByteArray(); + } +} +// [END example] diff --git a/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheSyncCacheServlet.java b/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheSyncCacheServlet.java new file mode 100644 index 00000000000..97307dc10e6 --- /dev/null +++ b/appengine-java8/memcache/src/main/java/com/example/appengine/memcache/MemcacheSyncCacheServlet.java @@ -0,0 +1,67 @@ +/** + * Copyright 2016 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.example.appengine.memcache; + +import com.google.appengine.api.memcache.ErrorHandlers; +import com.google.appengine.api.memcache.MemcacheService; +import com.google.appengine.api.memcache.MemcacheServiceFactory; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.logging.Level; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class MemcacheSyncCacheServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + String path = req.getRequestURI(); + if (path.startsWith("/favicon.ico")) { + return; // ignore the request for favicon.ico + } + + MemcacheService syncCache = MemcacheServiceFactory.getMemcacheService(); + syncCache.setErrorHandler(ErrorHandlers.getConsistentLogAndContinue(Level.INFO)); + String key = "count-sync"; + byte[] value; + long count = 1; + value = (byte[]) syncCache.get(key); + if (value == null) { + value = BigInteger.valueOf(count).toByteArray(); + syncCache.put(key, value); + } else { + // Increment value + count = new BigInteger(value).longValue(); + count++; + value = BigInteger.valueOf(count).toByteArray(); + // Put back in cache + syncCache.put(key, value); + } + + // Output content + resp.setContentType("text/plain"); + resp.getWriter().print("Value is " + count + "\n"); + } +} +// [END example] diff --git a/appengine-java8/memcache/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/memcache/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..71cecbdb5bb --- /dev/null +++ b/appengine-java8/memcache/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,20 @@ + + + + + + java8 + true + + diff --git a/appengine-java8/memcache/src/main/webapp/WEB-INF/web.xml b/appengine-java8/memcache/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..d4a7e38c981 --- /dev/null +++ b/appengine-java8/memcache/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,53 @@ + + + + + + + memcache-best-practice + com.example.appengine.memcache.MemcacheBestPracticeServlet + + + memcache-best-practice + / + + + memcache-async + com.example.appengine.memcache.MemcacheAsyncCacheServlet + + + memcache-async + /async + + + memcache-sync + com.example.appengine.memcache.MemcacheSyncCacheServlet + + + memcache-sync + /sync + + + memcache-concurrent + com.example.appengine.memcache.MemcacheConcurrentServlet + + + memcache-concurrent + /concurrent + + + diff --git a/appengine-java8/multitenancy/README.md b/appengine-java8/multitenancy/README.md new file mode 100644 index 00000000000..d11dd5df9ac --- /dev/null +++ b/appengine-java8/multitenancy/README.md @@ -0,0 +1,19 @@ +# Multitenancy Java sample + +Shows the usage of the Namespaces API. + +An App Engine guestbook using Java, Maven, and Objectify. + +Data access using [Objectify](https://github.com/objectify/objectify) + +Please ask questions on [Stackoverflow](http://stackoverflow.com/questions/tagged/google-app-engine) + +## Running Locally + +How do I, as a developer, start working on the project? + +1. `mvn clean appengine:run` + +## Deploying + +1. `mvn clean appengine:deploy diff --git a/appengine-java8/multitenancy/pom.xml b/appengine-java8/multitenancy/pom.xml new file mode 100644 index 00000000000..24113b7f6df --- /dev/null +++ b/appengine-java8/multitenancy/pom.xml @@ -0,0 +1,127 @@ + + + + + 4.0.0 + war + 1.0-SNAPSHOT + + com.example.appengine + appengine-multitenancy-j8 + + + 5.1.17 + 20.0 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + jstl + jstl + 1.2 + + + + + com.google.guava + guava + ${guava.version} + + + com.googlecode.objectify + objectify + ${objectify.version} + + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.32 + test + + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/multitenancy/src/main/java/com/example/appengine/Greeting.java b/appengine-java8/multitenancy/src/main/java/com/example/appengine/Greeting.java new file mode 100644 index 00000000000..c0645fc4fd8 --- /dev/null +++ b/appengine-java8/multitenancy/src/main/java/com/example/appengine/Greeting.java @@ -0,0 +1,76 @@ +/** + * Copyright 2014-2015 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. + */ + +//[START all] +package com.example.appengine; + +import com.googlecode.objectify.Key; +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; +import com.googlecode.objectify.annotation.Index; +import com.googlecode.objectify.annotation.Parent; + +import java.util.Date; + +/** + * The @Entity tells Objectify about our entity. We also register it in {@link OfyHelper} + * Our primary key @Id is set automatically by the Google Datastore for us. + * + * We add a @Parent to tell the object about its ancestor. We are doing this to support many + * guestbooks. Objectify, unlike the AppEngine library requires that you specify the fields you + * want to index using @Index. Only indexing the fields you need can lead to substantial gains in + * performance -- though if not indexing your data from the start will require indexing it later. + * + * NOTE - all the properties are PUBLIC so that can keep the code simple. + **/ +@Entity +public class Greeting { + @Parent Key theBook; + @Id public Long id; + + public String authorEmail; + public String authorId; + public String content; + @Index public Date date; + + /** + * Simple constructor just sets the date. + **/ + public Greeting() { + date = new Date(); + } + + /** + * A convenience constructor. + **/ + public Greeting(String book, String content) { + this(); + if ( book != null ) { + theBook = Key.create(Guestbook.class, book); // Creating the Ancestor key + } else { + theBook = Key.create(Guestbook.class, "default"); + } + this.content = content; + } + + public Greeting(String book, String content, String id, String email) { + this(book, content); + authorEmail = email; + authorId = id; + } + +} +//[END all] diff --git a/appengine-java8/multitenancy/src/main/java/com/example/appengine/Guestbook.java b/appengine-java8/multitenancy/src/main/java/com/example/appengine/Guestbook.java new file mode 100644 index 00000000000..1da82bc5b7f --- /dev/null +++ b/appengine-java8/multitenancy/src/main/java/com/example/appengine/Guestbook.java @@ -0,0 +1,33 @@ +/** + * Copyright 2014-2015 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. + */ + +//[START all] +package com.example.appengine; + +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; + +/** + * The @Entity tells Objectify about our entity. We also register it in + * OfyHelper.java -- very important. + * + * This is never actually created, but gives a hint to Objectify about our Ancestor key. + */ +@Entity +public class Guestbook { + @Id public String book; +} +//[END all] diff --git a/appengine-java8/multitenancy/src/main/java/com/example/appengine/MultitenancyServlet.java b/appengine-java8/multitenancy/src/main/java/com/example/appengine/MultitenancyServlet.java new file mode 100644 index 00000000000..ad206f06496 --- /dev/null +++ b/appengine-java8/multitenancy/src/main/java/com/example/appengine/MultitenancyServlet.java @@ -0,0 +1,114 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.google.appengine.api.NamespaceManager; +import com.google.appengine.api.memcache.MemcacheService; +import com.google.appengine.api.memcache.MemcacheServiceFactory; +import com.google.appengine.api.search.Index; +import com.google.appengine.api.search.IndexSpec; +import com.google.appengine.api.search.SearchService; +import com.google.appengine.api.search.SearchServiceConfig; +import com.google.appengine.api.search.SearchServiceFactory; +import com.google.appengine.api.users.UserServiceFactory; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class MultitenancyServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String namespace; + + PrintWriter out = resp.getWriter(); + out.println("Code Snippets -- not yet fully runnable as an app"); + + // [START temp_namespace] +// Set the namepace temporarily to "abc" + String oldNamespace = NamespaceManager.get(); + NamespaceManager.set("abc"); + try { +// ... perform operation using current namespace ... + } finally { + NamespaceManager.set(oldNamespace); + } +// [END temp_namespace] + + // [START per_user_namespace] + if (com.google.appengine.api.NamespaceManager.get() == null) { + // Assuming there is a logged in user. + namespace = UserServiceFactory.getUserService().getCurrentUser().getUserId(); + NamespaceManager.set(namespace); + } +// [END per_user_namespace] + String value = "something here"; + + // [START ns_memcache] + // Create a MemcacheService that uses the current namespace by + // calling NamespaceManager.get() for every access. + MemcacheService current = MemcacheServiceFactory.getMemcacheService(); + + // stores value in namespace "abc" + oldNamespace = NamespaceManager.get(); + NamespaceManager.set("abc"); + try { + current.put("key", value); // stores value in namespace “abc” + } finally { + NamespaceManager.set(oldNamespace); + } +// [END ns_memcache] + + // [START specific_memcache] + // Create a MemcacheService that uses the namespace "abc". + MemcacheService explicit = MemcacheServiceFactory.getMemcacheService("abc"); + explicit.put("key", value); // stores value in namespace "abc" + // [END specific_memcache] + + //[START searchns] + // Set the current namespace to "aSpace" + NamespaceManager.set("aSpace"); + // Create a SearchService with the namespace "aSpace" + SearchService searchService = SearchServiceFactory.getSearchService(); + // Create an IndexSpec + IndexSpec indexSpec = IndexSpec.newBuilder().setName("myIndex").build(); + // Create an Index with the namespace "aSpace" + Index index = searchService.getIndex(indexSpec); + // [END searchns] + + // [START searchns_2] + // Create a SearchServiceConfig, specifying the namespace "anotherSpace" + SearchServiceConfig config = SearchServiceConfig.newBuilder() + .setNamespace("anotherSpace").build(); + // Create a SearchService with the namespace "anotherSpace" + searchService = SearchServiceFactory.getSearchService(config); + // Create an IndexSpec + indexSpec = IndexSpec.newBuilder().setName("myindex").build(); + // Create an Index with the namespace "anotherSpace" + index = searchService.getIndex(indexSpec); + // [END searchns_2] + + } + + + +} +// [END example] diff --git a/appengine-java8/multitenancy/src/main/java/com/example/appengine/NamespaceFilter.java b/appengine-java8/multitenancy/src/main/java/com/example/appengine/NamespaceFilter.java new file mode 100644 index 00000000000..ed412169c99 --- /dev/null +++ b/appengine-java8/multitenancy/src/main/java/com/example/appengine/NamespaceFilter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.google.appengine.api.NamespaceManager; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +// [START nsfilter] +// Filter to set the Google Apps domain as the namespace. +public class NamespaceFilter implements Filter { + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + // Make sure set() is only called if the current namespace is not already set. + if (NamespaceManager.get() == null) { + // If your app is hosted on appspot, this will be empty. Otherwise it will be the domain + // the app is hosted on. + NamespaceManager.set(NamespaceManager.getGoogleAppsNamespace()); + } + chain.doFilter(req, res); // Pass request back down the filter chain + } +// [END nsfilter] + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void destroy() { + + } +} diff --git a/appengine-java8/multitenancy/src/main/java/com/example/appengine/OfyHelper.java b/appengine-java8/multitenancy/src/main/java/com/example/appengine/OfyHelper.java new file mode 100644 index 00000000000..8dcd38e8d1f --- /dev/null +++ b/appengine-java8/multitenancy/src/main/java/com/example/appengine/OfyHelper.java @@ -0,0 +1,40 @@ +/** + * Copyright 2014-2015 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. + */ +//[START all] +package com.example.appengine; + +import com.googlecode.objectify.ObjectifyService; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +/** + * OfyHelper, a ServletContextListener, is setup in web.xml to run before a JSP is run. This is + * required to let JSP's access Ofy. + **/ +public class OfyHelper implements ServletContextListener { + public void contextInitialized(ServletContextEvent event) { + // This will be invoked as part of a warmup request, or the first user request if no warmup + // request. + ObjectifyService.register(Guestbook.class); + ObjectifyService.register(Greeting.class); + } + + public void contextDestroyed(ServletContextEvent event) { + // App Engine does not currently invoke this method. + } +} +//[END all] diff --git a/appengine-java8/multitenancy/src/main/java/com/example/appengine/SignGuestbookServlet.java b/appengine-java8/multitenancy/src/main/java/com/example/appengine/SignGuestbookServlet.java new file mode 100644 index 00000000000..8726c91ce24 --- /dev/null +++ b/appengine-java8/multitenancy/src/main/java/com/example/appengine/SignGuestbookServlet.java @@ -0,0 +1,61 @@ +/** + * Copyright 2014-2015 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. + */ + +//[START all] +package com.example.appengine; + +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; + +import com.googlecode.objectify.ObjectifyService; + +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Form Handling Servlet - most of the action for this sample is in webapp/guestbook.jsp. + * It displays {@link Greeting}'s. + */ +public class SignGuestbookServlet extends HttpServlet { + + // Process the http POST of the form + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + Greeting greeting; + + UserService userService = UserServiceFactory.getUserService(); + User user = userService.getCurrentUser(); // Find out who the user is. + + String guestbookName = req.getParameter("guestbookName"); + String content = req.getParameter("content"); + if (user != null) { + greeting = new Greeting(guestbookName, content, user.getUserId(), user.getEmail()); + } else { + greeting = new Greeting(guestbookName, content); + } + + // Use Objectify to save the greeting and now() is used to make the call synchronously as we + // will immediately get a new page using redirect and we want the data to be present. + ObjectifyService.ofy().save().entity(greeting).now(); + + resp.sendRedirect("/guestbook.jsp?guestbookName=" + guestbookName); + } +} +//[END all] diff --git a/appengine-java8/multitenancy/src/main/java/com/example/appengine/SomeRequestServlet.java b/appengine-java8/multitenancy/src/main/java/com/example/appengine/SomeRequestServlet.java new file mode 100644 index 00000000000..4157164a1f4 --- /dev/null +++ b/appengine-java8/multitenancy/src/main/java/com/example/appengine/SomeRequestServlet.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.google.appengine.api.NamespaceManager; +import com.google.appengine.api.taskqueue.QueueFactory; +import com.google.appengine.api.taskqueue.TaskOptions; + +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START tq_3] +public class SomeRequestServlet extends HttpServlet { + // Handler for URL get requests. + @Override + protected void doGet(HttpServletRequest req, + HttpServletResponse resp) + throws IOException { + + // Increment the count for the current namespace asynchronously. + QueueFactory.getDefaultQueue().add( + TaskOptions.Builder.withUrl("/_ah/update_count") + .param("countName", "SomeRequest")); + // Increment the global count and set the + // namespace locally. The namespace is + // transferred to the invoked request and + // executed asynchronously. + String namespace = NamespaceManager.get(); + try { + NamespaceManager.set("-global-"); + QueueFactory.getDefaultQueue().add( + TaskOptions.Builder.withUrl("/_ah/update_count") + .param("countName", "SomeRequest")); + } finally { + NamespaceManager.set(namespace); + } + resp.setContentType("text/plain"); + resp.getWriter().println("Counts are being updated."); + } +} +// [END tq_3] \ No newline at end of file diff --git a/appengine-java8/multitenancy/src/main/java/com/example/appengine/UpdateCountsServlet.java b/appengine-java8/multitenancy/src/main/java/com/example/appengine/UpdateCountsServlet.java new file mode 100644 index 00000000000..cb1fbe8a3e3 --- /dev/null +++ b/appengine-java8/multitenancy/src/main/java/com/example/appengine/UpdateCountsServlet.java @@ -0,0 +1,99 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.googlecode.objectify.ObjectifyService.ofy; + +import com.google.appengine.api.NamespaceManager; + +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; +import com.googlecode.objectify.annotation.Index; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START datastore] +// [START tq_1] +public class UpdateCountsServlet extends HttpServlet { + private static final int NUM_RETRIES = 10; + + @Entity public class CounterPojo { + @Id public Long id; + @Index public String name; + public Long count; + + public CounterPojo() { + this.count = 0L; + } + + public CounterPojo(String name) { + this.name = name; + this.count = 0L; + } + + public void increment() { + count++; + } + } + + // Increment the count in a Counter datastore entity. + public long updateCount(String countName) { + + CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now(); + if (cp == null) { + cp = new CounterPojo(countName); + } + cp.increment(); + ofy().save().entity(cp).now(); + + return cp.count; + } +// [END tq_1] + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws java.io.IOException { + + // Update the count for the current namespace. + updateCount("request"); + + // Update the count for the "-global-" namespace. + String namespace = NamespaceManager.get(); + try { + // "-global-" is namespace reserved by the application. + NamespaceManager.set("-global-"); + updateCount("request"); + } finally { + NamespaceManager.set(namespace); + } + resp.setContentType("text/plain"); + resp.getWriter().println("Counts are now updated."); + } + // [END datastore] + + // [START tq_2] + // called from Task Queue + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + String[] countName = req.getParameterValues("countName"); + if (countName.length != 1) { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + updateCount(countName[0]); + } + // [END tq_2] +} diff --git a/appengine-java8/multitenancy/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/multitenancy/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..b85f72b69e3 --- /dev/null +++ b/appengine-java8/multitenancy/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,9 @@ + + + java8 + true + + + + + diff --git a/appengine-java8/multitenancy/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/multitenancy/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..a17206681f0 --- /dev/null +++ b/appengine-java8/multitenancy/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,13 @@ +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# + +# Set the default logging level for all loggers to WARNING +.level = WARNING diff --git a/appengine-java8/multitenancy/src/main/webapp/WEB-INF/web.xml b/appengine-java8/multitenancy/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..92cf8deccd3 --- /dev/null +++ b/appengine-java8/multitenancy/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,49 @@ + + + + + + sign + com.example.appengine.SignGuestbookServlet + + + + sign + /sign + + + + guestbook.jsp + + + + + + ObjectifyFilter + com.googlecode.objectify.ObjectifyFilter + + + ObjectifyFilter + /* + + + com.example.appengine.OfyHelper + + + + + + + NamespaceFilter + com.example.appengine.NamespaceFilter + + + + NamespaceFilter + /sign + + + + diff --git a/appengine-java8/multitenancy/src/main/webapp/guestbook.jsp b/appengine-java8/multitenancy/src/main/webapp/guestbook.jsp new file mode 100644 index 00000000000..317ba765ddc --- /dev/null +++ b/appengine-java8/multitenancy/src/main/webapp/guestbook.jsp @@ -0,0 +1,106 @@ +<%-- //[START all]--%> +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="com.google.appengine.api.users.User" %> +<%@ page import="com.google.appengine.api.users.UserService" %> +<%@ page import="com.google.appengine.api.users.UserServiceFactory" %> + +<%-- //[START imports]--%> +<%@ page import="com.example.appengine.Greeting" %> +<%@ page import="com.example.appengine.Guestbook" %> +<%@ page import="com.googlecode.objectify.Key" %> +<%@ page import="com.googlecode.objectify.ObjectifyService" %> +<%-- //[END imports]--%> + +<%@ page import="java.util.List" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + + + + + + + + +<% + String guestbookName = request.getParameter("guestbookName"); + if (guestbookName == null) { + guestbookName = "default"; + } + pageContext.setAttribute("guestbookName", guestbookName); + UserService userService = UserServiceFactory.getUserService(); + User user = userService.getCurrentUser(); + if (user != null) { + pageContext.setAttribute("user", user); +%> + +

Hello, ${fn:escapeXml(user.nickname)}! (You can + sign out.)

+<% + } else { +%> +

Hello! + Sign in + to include your name with greetings you post.

+<% + } +%> + +<%-- //[START datastore]--%> +<% + // Create the correct Ancestor key + Key theBook = Key.create(Guestbook.class, guestbookName); + + // Run an ancestor query to ensure we see the most up-to-date + // view of the Greetings belonging to the selected Guestbook. + List greetings = ObjectifyService.ofy() + .load() + .type(Greeting.class) // We want only Greetings + .ancestor(theBook) // Anyone in this book + .order("-date") // Most recent first - date is indexed. + .limit(5) // Only show 5 of them. + .list(); + + if (greetings.isEmpty()) { +%> +

Guestbook '${fn:escapeXml(guestbookName)}' has no messages.

+<% + } else { +%> +

Messages in Guestbook '${fn:escapeXml(guestbookName)}'.

+<% + // Look at all of our greetings + for (Greeting greeting : greetings) { + pageContext.setAttribute("greeting_content", greeting.content); + String author; + if (greeting.authorEmail == null) { + author = "An anonymous person"; + } else { + author = greeting.authorEmail; + String author_id = greeting.authorId; + if (user != null && user.getUserId().equals(author_id)) { + author += " (You)"; + } + } + pageContext.setAttribute("greeting_user", author); +%> +

${fn:escapeXml(greeting_user)} wrote:

+
${fn:escapeXml(greeting_content)}
+<% + } + } +%> + +
+
+
+ +
+<%-- //[END datastore]--%> +
+
+
+
+ + + +<%-- //[END all]--%> diff --git a/appengine-java8/multitenancy/src/main/webapp/stylesheets/main.css b/appengine-java8/multitenancy/src/main/webapp/stylesheets/main.css new file mode 100644 index 00000000000..05d72d5536d --- /dev/null +++ b/appengine-java8/multitenancy/src/main/webapp/stylesheets/main.css @@ -0,0 +1,4 @@ +body { + font-family: Verdana, Helvetica, sans-serif; + background-color: #FFFFCC; +} diff --git a/appengine-java8/multitenancy/src/test/java/com/example/appengine/GreetingTest.java b/appengine-java8/multitenancy/src/test/java/com/example/appengine/GreetingTest.java new file mode 100644 index 00000000000..b58b2f3d9f6 --- /dev/null +++ b/appengine-java8/multitenancy/src/test/java/com/example/appengine/GreetingTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.example.appengine.GuestbookTestUtilities.cleanDatastore; +import static org.junit.Assert.assertEquals; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import com.googlecode.objectify.ObjectifyService; +import com.googlecode.objectify.util.Closeable; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + + +@RunWith(JUnit4.class) +public class GreetingTest { + private static final String TEST_CONTENT = "The world is Blue today"; + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + private Closeable closeable; + private DatastoreService ds; + + @Before + public void setUp() throws Exception { + + helper.setUp(); + ds = DatastoreServiceFactory.getDatastoreService(); + + ObjectifyService.register(Guestbook.class); + ObjectifyService.register(Greeting.class); + + closeable = ObjectifyService.begin(); + + cleanDatastore(ds, "default"); + } + + @After + public void tearDown() { + cleanDatastore(ds, "default"); + helper.tearDown(); + closeable.close(); + } + + @Test + public void createSaveObject() throws Exception { + + Greeting g = new Greeting("default", TEST_CONTENT); + ObjectifyService.ofy().save().entity(g).now(); + + Query query = new Query("Greeting") + .setAncestor(new KeyFactory.Builder("Guestbook", "default").getKey()); + PreparedQuery pq = ds.prepare(query); + Entity greeting = pq.asSingleEntity(); // Should only be one at this point. + assertEquals(greeting.getProperty("content"), TEST_CONTENT); + } +} diff --git a/appengine-java8/multitenancy/src/test/java/com/example/appengine/GuestbookTestUtilities.java b/appengine-java8/multitenancy/src/test/java/com/example/appengine/GuestbookTestUtilities.java new file mode 100644 index 00000000000..6b2c41761d0 --- /dev/null +++ b/appengine-java8/multitenancy/src/test/java/com/example/appengine/GuestbookTestUtilities.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 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.example.appengine; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; + +import java.util.ArrayList; +import java.util.List; + +public class GuestbookTestUtilities { + + public static void cleanDatastore(DatastoreService ds, String book) { + Query query = new Query("Greeting") + .setAncestor(new KeyFactory.Builder("Guestbook", book) + .getKey()).setKeysOnly(); + PreparedQuery pq = ds.prepare(query); + List entities = pq.asList(FetchOptions.Builder.withDefaults()); + ArrayList keys = new ArrayList<>(entities.size()); + + for (Entity e : entities) { + keys.add(e.getKey()); + } + ds.delete(keys); + } + +} diff --git a/appengine-java8/multitenancy/src/test/java/com/example/appengine/SignGuestbookServletTest.java b/appengine-java8/multitenancy/src/test/java/com/example/appengine/SignGuestbookServletTest.java new file mode 100644 index 00000000000..7a532ca9d3f --- /dev/null +++ b/appengine-java8/multitenancy/src/test/java/com/example/appengine/SignGuestbookServletTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2015 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.example.appengine; + +import static com.example.appengine.GuestbookTestUtilities.cleanDatastore; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import com.googlecode.objectify.ObjectifyService; +import com.googlecode.objectify.util.Closeable; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link com.example.appengine.SignGuestbookServlet}. + */ +@RunWith(JUnit4.class) +public class SignGuestbookServletTest { + private static final String FAKE_URL = "fakey.org/sign"; + private static final String FAKE_NAME = "Fake"; + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper( + // Set no eventual consistency, that way queries return all results. + // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests + new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(0)); + + private final String testPhrase = "Noew is the time"; + + @Mock private HttpServletRequest mockRequest; + + @Mock + private HttpServletResponse mockResponse; + + private StringWriter stringWriter; + private SignGuestbookServlet servletUnderTest; + private Closeable closeable; + private DatastoreService ds; + + @Before + public void setUp() throws Exception { + + MockitoAnnotations.initMocks(this); + helper.setUp(); + ds = DatastoreServiceFactory.getDatastoreService(); + + // Set up some fake HTTP requests + when(mockRequest.getRequestURI()).thenReturn(FAKE_URL); + when(mockRequest.getParameter("guestbookName")).thenReturn( "default" ); + when(mockRequest.getParameter("content")).thenReturn( testPhrase ); + + stringWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(stringWriter)); + + servletUnderTest = new SignGuestbookServlet(); + + ObjectifyService.register(Guestbook.class); + ObjectifyService.register(Greeting.class); + + closeable = ObjectifyService.begin(); + + cleanDatastore(ds, "default"); + } + + @After public void tearDown() { + cleanDatastore(ds, "default"); + helper.tearDown(); + closeable.close(); + } + + @Test + public void doPost_userNotLoggedIn() throws Exception { + servletUnderTest.doPost(mockRequest, mockResponse); + + Query query = new Query("Greeting") + .setAncestor(new KeyFactory.Builder("Guestbook", "default").getKey()); + PreparedQuery pq = ds.prepare(query); + + Entity greeting = pq.asSingleEntity(); // Should only be one at this point. + assertEquals(greeting.getProperty("content"), testPhrase); + } + +} diff --git a/appengine-java8/oauth2/README.md b/appengine-java8/oauth2/README.md new file mode 100644 index 00000000000..3f38cfba462 --- /dev/null +++ b/appengine-java8/oauth2/README.md @@ -0,0 +1,41 @@ +# Google App Engine Standard Environment +## Oauth2 Sample + +This sample demonstrates using the Oauth2 apis to create an authenticaion filter. + +See the [Google App Engine standard environment documentation][ae-docs] for more +detailed instructions. + + +## Setup +1. In the [Cloud Developers Console](https://cloud.google.com/console) > API Manager > Credentials, +create a Oauth Client ID for a Web Application. You will need to provide an authroized JavaScript +origin. Typically, https://projectID.appspot.com. +1. Edit `src/main/webapp/index.html` and change `YOUR_CLIENT_ID_HERE.apps.googleusercontent.com` to +Client ID from the prior step. + +## Running locally +NOTE: The app can be run locally, but the Oauth2 APIs do not work with the development server. + + $ mvn appengine:devserver + +## Deploying + $ mvn appengine:update -Dappengine.appId=YOUR-PROJECT-ID -Dappengine.version=SOME-VERSION + +1. Using your browser, visit `https://YOUR-PROJECT-ID.appspot.com`, click Sign In. + +1. The Sign In process will then request some text from your app, and then display it, if +the id matches the list in `src/main/java/com/example/appengine/Oauth2Filter.java`. + +## Adding you to the list of valid users +NOTE: Typically, you would use this for Service Accounts, but user accounts work as well. + +1. Enable logging by uncommenting the context.log line in +`src/main/java/com/example/appengine/Oauth2Filter.java`, redeploy, and visit the page +1. Look at the logs in [Cloud Developers Console](https://cloud.google.com/console) > Logs. + +1. Add the `tokenAudience` to the `allowedClients`. + +1. Deploy and visit the page again. + +[ae-docs]: https://cloud.google.com/appengine/docs/java/ diff --git a/appengine-java8/oauth2/pom.xml b/appengine-java8/oauth2/pom.xml new file mode 100644 index 00000000000..7a343adf8d8 --- /dev/null +++ b/appengine-java8/oauth2/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-oauth2-j8 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + com.google.guava + guava + 20.0 + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/oauth2/src/main/java/com/example/appengine/HelloServlet.java b/appengine-java8/oauth2/src/main/java/com/example/appengine/HelloServlet.java new file mode 100644 index 00000000000..35e0e1762ed --- /dev/null +++ b/appengine-java8/oauth2/src/main/java/com/example/appengine/HelloServlet.java @@ -0,0 +1,56 @@ +/** + * Copyright 2015 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.example.appengine; + +import com.google.appengine.api.oauth.OAuthRequestException; +import com.google.appengine.api.oauth.OAuthService; +import com.google.appengine.api.oauth.OAuthServiceFactory; +import com.google.appengine.api.users.User; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class HelloServlet extends HttpServlet { + + @Override + public void doPost(final HttpServletRequest req, final HttpServletResponse resp) + throws IOException { + + resp.setContentType("text/plain"); + PrintWriter out = resp.getWriter(); + + final String scope = "https://www.googleapis.com/auth/userinfo.email"; + OAuthService oauth = OAuthServiceFactory.getOAuthService(); + User user = null; + try { + user = oauth.getCurrentUser(scope); + } catch (OAuthRequestException e) { + getServletContext().log("Oauth error", e); + out.print("auth error"); + return; + } + + out.print("Hello world, welcome to Oauth2: " + user.getEmail()); + } +} +// [END example] diff --git a/appengine-java8/oauth2/src/main/java/com/example/appengine/Oauth2Filter.java b/appengine-java8/oauth2/src/main/java/com/example/appengine/Oauth2Filter.java new file mode 100644 index 00000000000..f5cf8a969da --- /dev/null +++ b/appengine-java8/oauth2/src/main/java/com/example/appengine/Oauth2Filter.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016 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.example.appengine; + +import static com.google.appengine.api.utils.SystemProperty.environment; + +import com.google.appengine.api.oauth.OAuthRequestException; +import com.google.appengine.api.oauth.OAuthService; +import com.google.appengine.api.oauth.OAuthServiceFactory; +import com.google.appengine.api.oauth.OAuthServiceFailureException; +import com.google.appengine.api.utils.SystemProperty; +import com.google.common.collect.ImmutableSet; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +/** + * Filter to verify that request has a "Authorization: Bearer xxxx" header, + * and check if xxxx is authorized to use this app. + * + *

Note - this is to demonstrate the OAuth2 APIs, as it is possible to lockdown some + * of your app's URL's using cloud console by adding service accounts to the project.

+ */ +public class Oauth2Filter implements Filter { + + private ServletContext context; + + @Override + public void init(final FilterConfig config) throws ServletException { + this.context = config.getServletContext(); + } + + // [START oauth2] + @Override + public void doFilter( + final ServletRequest servletReq, final ServletResponse servletResp, final FilterChain chain) + throws IOException, ServletException { + final String scope = "https://www.googleapis.com/auth/userinfo.email"; + ImmutableSet allowedClients = new ImmutableSet.Builder() + .add("407408718192.apps.googleusercontent.com") + .add("755878275993-j4k7emq6rlupctce1c28enpcrr50vfo1.apps.googleusercontent.com") + .build(); + + HttpServletResponse resp = (HttpServletResponse) servletResp; + + OAuthService oauth = OAuthServiceFactory.getOAuthService(); + + // Only check Oauth2 when in production, skip if run in development. + SystemProperty.Environment.Value env = environment.value(); + if (env == SystemProperty.Environment.Value.Production) { // APIs only work in Production + try { + String tokenAudience = oauth.getClientId(scope); + + // The line below is commented out for privacy. +// context.log("tokenAudience: " + tokenAudience); // Account we match + + if (!allowedClients.contains(tokenAudience)) { + throw new OAuthRequestException("audience of token '" + tokenAudience + + "' is not in allowed list " + allowedClients); + } + } catch (OAuthRequestException ex) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); // Not allowed + return; + } catch (OAuthServiceFailureException ex) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); // some failure - reject + context.log("oauth2 failure", ex); + return; + } + } + chain.doFilter(servletReq, servletResp); // continue processing + } + // [END oauth2] + + @Override + public void destroy() { } + +} diff --git a/appengine-java8/oauth2/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/oauth2/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..d64ab6aafb7 --- /dev/null +++ b/appengine-java8/oauth2/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,19 @@ + + + + + + java8 + true + diff --git a/appengine-java8/oauth2/src/main/webapp/WEB-INF/web.xml b/appengine-java8/oauth2/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..183e631c600 --- /dev/null +++ b/appengine-java8/oauth2/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,27 @@ + + + + Oauth2Filter + com.example.appengine.Oauth2Filter + + + Oauth2Filter + /hello + + + + hello + com.example.appengine.HelloServlet + + + hello + /hello + + + + index.html + + diff --git a/appengine-java8/oauth2/src/main/webapp/index.html b/appengine-java8/oauth2/src/main/webapp/index.html new file mode 100644 index 00000000000..cb2401db6ab --- /dev/null +++ b/appengine-java8/oauth2/src/main/webapp/index.html @@ -0,0 +1,53 @@ + + + + + + + + appengine-oauth2 sample + + + + + +
+ + + diff --git a/appengine-java8/pom.xml b/appengine-java8/pom.xml new file mode 100644 index 00000000000..96050a85620 --- /dev/null +++ b/appengine-java8/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + 1.0.0 + + com.google.cloud + appengine-java8-samples + pom + + + + doc-samples + com.google.cloud + 1.0.0 + .. + + + + 1.9.52 + + + + + analytics + appidentity + cloudsql + + datastore + datastore/indexes + datastore/indexes-exploding + datastore/indexes-perfect + + endpoints-frameworks-v2/backend + + firebase-event-proxy/gae-firebase-event-proxy + + firebase-tictactoe + + guestbook-cloud-datastore + guestbook-objectify + + helloworld + + images + logs + mailgun + mailjet + memcache + multitenancy + oauth2 + requests + search + sendgrid + + remote/remote-client + remote/remote-server + + static-files + + taskqueue/deferred + taskqueue/pull + taskqueue/push + + twilio + + unittests + + urlfetch + users + + + + diff --git a/appengine-java8/remote/README.md b/appengine-java8/remote/README.md new file mode 100644 index 00000000000..1cff7ec12ec --- /dev/null +++ b/appengine-java8/remote/README.md @@ -0,0 +1,21 @@ +# Google App Engine Standard Environment Remote API Sample + +This sample demonstrates how to access App Engine Standard Environment APIs remotely, +using the [Remote API](https://cloud.google.com/appengine/docs/java/tools/remoteapi). + +## Set up the server component of Remote API +1. `gcloud init` +1. Navigate to the remote-server directory +1. Deploy the app + `mvn appengine:deploy` +1. Alternatively, run the app locally with + `mvn appengine:run` +## Set up the client component of Remote API +1. Package the app as a jar + `mvn clean package` +1. Navigate to the target directory +1. Excute the jar file with the server connection string as the first argument + 1. If you deployed the app, it should be "YOUR-APP-ID.appspot.com" + 1. If you are running on the development server, it should be "localhost:8080" + java -jar appengine-remote-client-1.0-SNAPSHOT-jar-with-dependencies.jar "YOUR-APP-NAME" + diff --git a/appengine-java8/remote/remote-client/pom.xml b/appengine-java8/remote/remote-client/pom.xml new file mode 100644 index 00000000000..0f65dbfceee --- /dev/null +++ b/appengine-java8/remote/remote-client/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + jar + 1.0-SNAPSHOT + com.example.appengine + appengine-remote-client-j8 + + + appengine-java8-samples + com.google.cloud + 1.0.0 + ../.. + + + + + + com.google.appengine + appengine-remote-api + ${appengine.sdk.version} + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + + maven-assembly-plugin + + + package + + single + + + + + + + com.example.appengine.remote.RemoteApiExample + + + + jar-with-dependencies + + + + + + diff --git a/appengine-java8/remote/remote-client/src/main/java/com/example/appengine/remote/RemoteApiExample.java b/appengine-java8/remote/remote-client/src/main/java/com/example/appengine/remote/RemoteApiExample.java new file mode 100644 index 00000000000..97131664214 --- /dev/null +++ b/appengine-java8/remote/remote-client/src/main/java/com/example/appengine/remote/RemoteApiExample.java @@ -0,0 +1,50 @@ +/** + * Copyright 2016 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.example.appengine.remote; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.tools.remoteapi.RemoteApiInstaller; +import com.google.appengine.tools.remoteapi.RemoteApiOptions; + +import java.io.IOException; + +// [START example] +public class RemoteApiExample { + + public static void main(String[] args) throws IOException { + String serverString = args[0]; + RemoteApiOptions options; + if (serverString.equals("localhost")) { + options = new RemoteApiOptions().server(serverString, + 8080).useDevelopmentServerCredential(); + } else { + options = new RemoteApiOptions().server(serverString, + 443).useApplicationDefaultCredential(); + } + RemoteApiInstaller installer = new RemoteApiInstaller(); + installer.install(options); + try { + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + System.out.println("Key of new entity is " + ds.put(new Entity("Hello Remote API!"))); + } finally { + installer.uninstall(); + } + } +} +//[END example] diff --git a/appengine-java8/remote/remote-server/pom.xml b/appengine-java8/remote/remote-server/pom.xml new file mode 100644 index 00000000000..1eff745a61e --- /dev/null +++ b/appengine-java8/remote/remote-server/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-remote-server-j8 + + + appengine-java8-samples + com.google.cloud + 1.0.0 + ../.. + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + + diff --git a/appengine-java8/remote/remote-server/src/main/java/com/example/appengine/remote/RemoteServlet.java b/appengine-java8/remote/remote-server/src/main/java/com/example/appengine/remote/RemoteServlet.java new file mode 100644 index 00000000000..31e2b06f823 --- /dev/null +++ b/appengine-java8/remote/remote-server/src/main/java/com/example/appengine/remote/RemoteServlet.java @@ -0,0 +1,36 @@ +/** + * Copyright 2016 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.example.appengine.remote; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class RemoteServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + PrintWriter out = resp.getWriter(); + out.println("Hello, world"); + } +} +// [END example] diff --git a/appengine-java8/remote/remote-server/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/remote/remote-server/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..71cecbdb5bb --- /dev/null +++ b/appengine-java8/remote/remote-server/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,20 @@ + + + + + + java8 + true + + diff --git a/appengine-java8/remote/remote-server/src/main/webapp/WEB-INF/web.xml b/appengine-java8/remote/remote-server/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..0ec344f02b5 --- /dev/null +++ b/appengine-java8/remote/remote-server/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,41 @@ + + + + + + + + Remote API Servlet + RemoteApiServlet + com.google.apphosting.utils.remoteapi.RemoteApiServlet + 1 + + + RemoteApiServlet + /remote_api + + + + remote + com.example.appengine.remote.RemoteServlet + + + remote + / + + + diff --git a/appengine-java8/requests/README.md b/appengine-java8/requests/README.md new file mode 100644 index 00000000000..a8f353d3838 --- /dev/null +++ b/appengine-java8/requests/README.md @@ -0,0 +1,31 @@ +# Request Handling sample for Google App Engine + +This sample provides Java code samples in support of the "Handling Requests" description [Requests][requests-doc] on [Google App +Engine][ae-docs]. + +[requests-doc]: https://cloud.google.com/appengine/docs/java/requests +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Setup + + gcloud init + +## Running locally +This example uses the +[Cloud SDK Maven plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven). +To run this sample locally: + + mvn appengine:run + +To see the results of the RequestsServlet, open `localhost:8080` in a WWW browser. + +To see the results of the LoggingServlet, open `localhost:8080/logs` in a WWW browser +and examine the logs to see the actual messages. + +## Deploying +In the following command, replace YOUR-PROJECT-ID with your +[Google Cloud Project ID](https://developers.google.com/console/help/new/#projectnumber) +and SOME-VERSION with a valid version number. + + mvn appengine:deploy + diff --git a/appengine-java8/requests/pom.xml b/appengine-java8/requests/pom.xml new file mode 100644 index 00000000000..9839855623c --- /dev/null +++ b/appengine-java8/requests/pom.xml @@ -0,0 +1,112 @@ + + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-requests-j8 + + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + + com.google.appengine + appengine-maven-plugin + ${appengine.sdk.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + com.google.guava + guava + 20.0 + + + + org.json + json + 20160810 + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.32 + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/requests/src/main/java/com/example/appengine/requests/LoggingServlet.java b/appengine-java8/requests/src/main/java/com/example/appengine/requests/LoggingServlet.java new file mode 100644 index 00000000000..213ab065aab --- /dev/null +++ b/appengine-java8/requests/src/main/java/com/example/appengine/requests/LoggingServlet.java @@ -0,0 +1,41 @@ +/* Copyright 2016 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.example.appengine.requests; + +import java.io.IOException; +import java.util.logging.Logger; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START simple_logging_example] +public class LoggingServlet extends HttpServlet { + private static final Logger log = Logger.getLogger(LoggingServlet.class.getName()); + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + log.info("An informational message."); + log.warning("A warning message."); + log.severe("An error message."); + // [START_EXCLUDE] + resp.setContentType("text/plain"); + resp.getWriter().println("Check logs for results"); + // [END_EXCLUDE] + } +} +// [END simple_logging_example] + diff --git a/appengine-java8/requests/src/main/java/com/example/appengine/requests/RequestsServlet.java b/appengine-java8/requests/src/main/java/com/example/appengine/requests/RequestsServlet.java new file mode 100644 index 00000000000..82c649e0eab --- /dev/null +++ b/appengine-java8/requests/src/main/java/com/example/appengine/requests/RequestsServlet.java @@ -0,0 +1,33 @@ +/* Copyright 2016 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.example.appengine.requests; + +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START simple_request_example] +public class RequestsServlet extends HttpServlet { + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + resp.setContentType("text/plain"); + resp.getWriter().println("Hello, world"); + } +} +// [END simple_request_example] + diff --git a/appengine-java8/requests/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/requests/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..29bc35cee0b --- /dev/null +++ b/appengine-java8/requests/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,5 @@ + + + java8 + true + diff --git a/appengine-java8/requests/src/main/webapp/WEB-INF/web.xml b/appengine-java8/requests/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..7784c6a56c6 --- /dev/null +++ b/appengine-java8/requests/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,26 @@ + + + + requests + com.example.appengine.requests.RequestsServlet + + + logging + com.example.appengine.requests.LoggingServlet + + + requests + / + + + requests + /requests + + + logging + /logs + + diff --git a/appengine-java8/requests/src/test/java/com/example/appengine/requests/LoggingServletTest.java b/appengine-java8/requests/src/test/java/com/example/appengine/requests/LoggingServletTest.java new file mode 100644 index 00000000000..6f3469e6b5c --- /dev/null +++ b/appengine-java8/requests/src/test/java/com/example/appengine/requests/LoggingServletTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2016 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.example.appengine.requests; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link LoggingServlet}. + */ +@RunWith(JUnit4.class) +public class LoggingServletTest { + // To capture and restore stderr + private final ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + private static final PrintStream REAL_ERR = System.err; + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private LoggingServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + // Capture stderr to examine messages written to it + System.setErr(new PrintStream(stderr)); + + MockitoAnnotations.initMocks(this); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new LoggingServlet(); + } + + @After + public void tearDown() { + // Restore stderr + System.setErr(LoggingServletTest.REAL_ERR); + } + + @Test + public void testListLogs() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + String out = stderr.toString(); + + // We expect three log messages to be created + // with the following messages. + assertThat(out).contains("An informational message."); + assertThat(out).contains("A warning message."); + assertThat(out).contains("An error message."); + } + +} diff --git a/appengine-java8/requests/src/test/java/com/example/appengine/requests/RequestsServletTest.java b/appengine-java8/requests/src/test/java/com/example/appengine/requests/RequestsServletTest.java new file mode 100644 index 00000000000..639aa0a6a2a --- /dev/null +++ b/appengine-java8/requests/src/test/java/com/example/appengine/requests/RequestsServletTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016 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.example.appengine.requests; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link RequestsServlet}. + */ +@RunWith(JUnit4.class) +public class RequestsServletTest { + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private RequestsServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new RequestsServlet(); + } + + @Test + public void doGet_writesResponse() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + + // We expect a greeting to be returned. + assertThat(responseWriter.toString()) + .named("RequestsServlet response") + .contains("Hello, world"); + } +} diff --git a/appengine-java8/search/README.md b/appengine-java8/search/README.md new file mode 100644 index 00000000000..34121fac71e --- /dev/null +++ b/appengine-java8/search/README.md @@ -0,0 +1,20 @@ +# Google App Engine Standard Environment Search API Sample + +This sample demonstrates how to use App Engine Search API. + +See the [Google App Engine Search API documentation][search-api-docs] for more +detailed instructions. + +[search-api-docs]: https://cloud.google.com/appengine/docs/java/search/ + +## Setup +1. Update the `` tag in `src/main/webapp/WEB-INF/appengine-web.xml` + with your project name. +1. Update the `` tag in `src/main/webapp/WEB-INF/appengine-web.xml` + with your version name. + +## Running locally + $ mvn appengine:devserver + +## Deploying + $ mvn appengine:update \ No newline at end of file diff --git a/appengine-java8/search/pom.xml b/appengine-java8/search/pom.xml new file mode 100644 index 00000000000..76973c9c88f --- /dev/null +++ b/appengine-java8/search/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-search-j8 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.32 + test + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/search/src/main/java/com/example/appengine/search/DeleteServlet.java b/appengine-java8/search/src/main/java/com/example/appengine/search/DeleteServlet.java new file mode 100644 index 00000000000..da9fb6125d3 --- /dev/null +++ b/appengine-java8/search/src/main/java/com/example/appengine/search/DeleteServlet.java @@ -0,0 +1,94 @@ +/** + * Copyright 2016 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.example.appengine.search; + +// @formatter:off +// [START delete_import] +import com.google.appengine.api.search.Document; +import com.google.appengine.api.search.GetRequest; +import com.google.appengine.api.search.GetResponse; +// [END delete_import] + +// CHECKSTYLE:OFF +import com.google.appengine.api.search.Field; +import com.google.appengine.api.search.Index; +import com.google.appengine.api.search.IndexSpec; +import com.google.appengine.api.search.SearchServiceFactory; +// @formatter:on +// CHECKSTYLE:ON + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Code snippet for deleting documents from an Index. + */ +@SuppressWarnings("serial") +public class DeleteServlet extends HttpServlet { + private static final Logger LOG = Logger.getLogger(DeleteServlet.class.getSimpleName()); + + private static final String SEARCH_INDEX = "searchIndexForDelete"; + + private Index getIndex() { + IndexSpec indexSpec = IndexSpec.newBuilder().setName(SEARCH_INDEX).build(); + Index index = SearchServiceFactory.getSearchService().getIndex(indexSpec); + return index; + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + // Put one document to avoid an error + Document document = Document.newBuilder() + .addField(Field.newBuilder().setName("f").setText("v")) + .build(); + try { + Utils.indexADocument(SEARCH_INDEX, document); + } catch (InterruptedException e) { + // ignore + } + // [START delete_documents] + try { + // looping because getRange by default returns up to 100 documents at a time + while (true) { + List docIds = new ArrayList<>(); + // Return a set of doc_ids. + GetRequest request = GetRequest.newBuilder().setReturningIdsOnly(true).build(); + GetResponse response = getIndex().getRange(request); + if (response.getResults().isEmpty()) { + break; + } + for (Document doc : response) { + docIds.add(doc.getId()); + } + getIndex().delete(docIds); + } + } catch (RuntimeException e) { + LOG.log(Level.SEVERE, "Failed to delete documents", e); + } + // [END delete_documents] + PrintWriter out = resp.getWriter(); + out.println("Deleted documents."); + } +} diff --git a/appengine-java8/search/src/main/java/com/example/appengine/search/DocumentServlet.java b/appengine-java8/search/src/main/java/com/example/appengine/search/DocumentServlet.java new file mode 100644 index 00000000000..eaa26cd7388 --- /dev/null +++ b/appengine-java8/search/src/main/java/com/example/appengine/search/DocumentServlet.java @@ -0,0 +1,83 @@ +/** + * Copyright 2016 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.example.appengine.search; + +// [START document_import] +import com.google.appengine.api.search.Document; +import com.google.appengine.api.search.Field; +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserServiceFactory; +// [END document_import] + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Date; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * A servlet for creating Search API Document. + */ +@SuppressWarnings("serial") +public class DocumentServlet extends HttpServlet { + + /** + * Code snippet for creating a Document. + * @return Document Created document. + */ + public Document createDocument() { + // [START create_document] + User currentUser = UserServiceFactory.getUserService().getCurrentUser(); + String userEmail = currentUser == null ? "" : currentUser.getEmail(); + String userDomain = currentUser == null ? "" : currentUser.getAuthDomain(); + String myDocId = "PA6-5000"; + Document doc = Document.newBuilder() + // Setting the document identifer is optional. + // If omitted, the search service will create an identifier. + .setId(myDocId) + .addField(Field.newBuilder().setName("content").setText("the rain in spain")) + .addField(Field.newBuilder().setName("email").setText(userEmail)) + .addField(Field.newBuilder().setName("domain").setAtom(userDomain)) + .addField(Field.newBuilder().setName("published").setDate(new Date())) + .build(); + // [END create_document] + return doc; + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + PrintWriter out = resp.getWriter(); + Document document = Document.newBuilder() + .addField(Field.newBuilder().setName("coverLetter").setText("CoverLetter")) + .addField(Field.newBuilder().setName("resume").setHTML("")) + .addField(Field.newBuilder().setName("fullName").setAtom("Foo Bar")) + .addField(Field.newBuilder().setName("submissionDate").setDate(new Date())) + .build(); + // [START access_document] + String coverLetter = document.getOnlyField("coverLetter").getText(); + String resume = document.getOnlyField("resume").getHTML(); + String fullName = document.getOnlyField("fullName").getAtom(); + Date submissionDate = document.getOnlyField("submissionDate").getDate(); + // [END access_document] + out.println("coverLetter: " + coverLetter); + out.println("resume: " + resume); + out.println("fullName: " + fullName); + out.println("submissionDate: " + submissionDate.toString()); + } +} diff --git a/appengine-java8/search/src/main/java/com/example/appengine/search/IndexServlet.java b/appengine-java8/search/src/main/java/com/example/appengine/search/IndexServlet.java new file mode 100644 index 00000000000..390b74c2d5e --- /dev/null +++ b/appengine-java8/search/src/main/java/com/example/appengine/search/IndexServlet.java @@ -0,0 +1,75 @@ +/** + * Copyright 2016 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.example.appengine.search; + +// @formatter:off +import com.google.appengine.api.search.Document; +import com.google.appengine.api.search.Field; +import com.google.appengine.api.search.Index; +import com.google.appengine.api.search.IndexSpec; +import com.google.appengine.api.search.SearchServiceFactory; + +// CHECKSTYLE:OFF +// [START get_document_import] +import com.google.appengine.api.search.GetRequest; +import com.google.appengine.api.search.GetResponse; +// [END get_document_import] +// @formatter:on +// CHECKSTYLE:ON + +import java.io.IOException; +import java.io.PrintWriter; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * Code snippet for getting a document from Index. + */ +@SuppressWarnings("serial") +public class IndexServlet extends HttpServlet { + + private static final String INDEX = "testIndex"; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + PrintWriter out = resp.getWriter(); + Document document = Document.newBuilder() + .setId("AZ125") + .addField(Field.newBuilder().setName("myField").setText("myValue")).build(); + try { + Utils.indexADocument(INDEX, document); + } catch (InterruptedException e) { + out.println("Interrupted"); + return; + } + out.println("Indexed a new document."); + // [START get_document] + IndexSpec indexSpec = IndexSpec.newBuilder().setName(INDEX).build(); + Index index = SearchServiceFactory.getSearchService().getIndex(indexSpec); + + // Fetch a single document by its doc_id + Document doc = index.get("AZ125"); + + // Fetch a range of documents by their doc_ids + GetResponse docs = index.getRange( + GetRequest.newBuilder().setStartId("AZ125").setLimit(100).build()); + // [END get_document] + out.println("myField: " + docs.getResults().get(0).getOnlyField("myField").getText()); + } +} diff --git a/appengine-java8/search/src/main/java/com/example/appengine/search/SchemaServlet.java b/appengine-java8/search/src/main/java/com/example/appengine/search/SchemaServlet.java new file mode 100644 index 00000000000..4deda3e3cfa --- /dev/null +++ b/appengine-java8/search/src/main/java/com/example/appengine/search/SchemaServlet.java @@ -0,0 +1,81 @@ +/** + * Copyright 2016 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.example.appengine.search; + +import com.google.appengine.api.search.Document; +import com.google.appengine.api.search.Field; +import com.google.appengine.api.search.SearchServiceFactory; + +// @formatter:off +// CHECKSTYLE:OFF +// [START schema_import] +import com.google.appengine.api.search.Field.FieldType; +import com.google.appengine.api.search.Index; +import com.google.appengine.api.search.GetIndexesRequest; +import com.google.appengine.api.search.GetResponse; +import com.google.appengine.api.search.Schema; +// [END schema_import] +// @formatter:on +// CHECKSTYLE:ON + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +@SuppressWarnings("serial") +public class SchemaServlet extends HttpServlet { + + private static final String SEARCH_INDEX = "schemaIndex"; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + PrintWriter out = resp.getWriter(); + Document doc = Document.newBuilder() + .setId("theOnlyCar") + .addField(Field.newBuilder().setName("maker").setText("Toyota")) + .addField(Field.newBuilder().setName("price").setNumber(300000)) + .addField(Field.newBuilder().setName("color").setText("lightblue")) + .addField(Field.newBuilder().setName("model").setText("Prius")) + .build(); + try { + Utils.indexADocument(SEARCH_INDEX, doc); + } catch (InterruptedException e) { + // ignore + } + // [START list_schema] + GetResponse response = SearchServiceFactory.getSearchService().getIndexes( + GetIndexesRequest.newBuilder().setSchemaFetched(true).build()); + + // List out elements of each Schema + for (Index index : response) { + Schema schema = index.getSchema(); + for (String fieldName : schema.getFieldNames()) { + List typesForField = schema.getFieldTypes(fieldName); + // Just printing out the field names and types + for (FieldType type : typesForField) { + out.println(index.getName() + ":" + fieldName + ":" + type.name()); + } + } + } + // [END list_schema] + } +} diff --git a/appengine-java8/search/src/main/java/com/example/appengine/search/SearchOptionServlet.java b/appengine-java8/search/src/main/java/com/example/appengine/search/SearchOptionServlet.java new file mode 100644 index 00000000000..83a63287fa5 --- /dev/null +++ b/appengine-java8/search/src/main/java/com/example/appengine/search/SearchOptionServlet.java @@ -0,0 +1,126 @@ +/** + * Copyright 2016 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.example.appengine.search; + +import com.google.appengine.api.search.Document; +import com.google.appengine.api.search.Index; + +// CHECKSTYLE:OFF +// @formatter:off +// [START search_option_import] +import com.google.appengine.api.search.Field; +import com.google.appengine.api.search.IndexSpec; +import com.google.appengine.api.search.SearchServiceFactory; +import com.google.appengine.api.search.Query; +import com.google.appengine.api.search.QueryOptions; +import com.google.appengine.api.search.Results; +import com.google.appengine.api.search.SearchException; +import com.google.appengine.api.search.SortExpression; +import com.google.appengine.api.search.SortOptions; +import com.google.appengine.api.search.ScoredDocument; +// [END search_option_import] +// @formatter:on +// CHECKSTYLE:ON + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.logging.Logger; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * Code snippet for searching with query options. + */ +@SuppressWarnings("serial") +public class SearchOptionServlet extends HttpServlet { + private static final Logger LOG = Logger.getLogger(SearchOptionServlet.class.getSimpleName()); + + private static final String SEARCH_INDEX = "searchOptionIndex"; + + private Index getIndex() { + IndexSpec indexSpec = IndexSpec.newBuilder().setName(SEARCH_INDEX).build(); + Index index = SearchServiceFactory.getSearchService().getIndex(indexSpec); + return index; + } + + private Results doSearch() { + String indexName = SEARCH_INDEX; + // [START search_with_options] + try { + // Build the SortOptions with 2 sort keys + SortOptions sortOptions = SortOptions.newBuilder() + .addSortExpression(SortExpression.newBuilder() + .setExpression("price") + .setDirection(SortExpression.SortDirection.DESCENDING) + .setDefaultValueNumeric(0)) + .addSortExpression(SortExpression.newBuilder() + .setExpression("brand") + .setDirection(SortExpression.SortDirection.DESCENDING) + .setDefaultValue("")) + .setLimit(1000) + .build(); + + // Build the QueryOptions + QueryOptions options = QueryOptions.newBuilder() + .setLimit(25) + .setFieldsToReturn("model", "price", "description") + .setSortOptions(sortOptions) + .build(); + + // A query string + String queryString = "product: coffee roaster AND price < 500"; + + // Build the Query and run the search + Query query = Query.newBuilder().setOptions(options).build(queryString); + IndexSpec indexSpec = IndexSpec.newBuilder().setName(indexName).build(); + Index index = SearchServiceFactory.getSearchService().getIndex(indexSpec); + Results result = index.search(query); + return result; + } catch (SearchException e) { + // handle exception... + } + // [END search_with_options] + return null; + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + // Put one document to avoid an error + Document document = Document.newBuilder() + .setId("theOnlyCoffeeRoaster") + .addField(Field.newBuilder().setName("price").setNumber(200)) + .addField(Field.newBuilder().setName("model").setText("TZ4000")) + .addField(Field.newBuilder().setName("brand").setText("MyBrand")) + .addField(Field.newBuilder().setName("product").setText("coffee roaster")) + .addField(Field.newBuilder() + .setName("description").setText("A coffee bean roaster at home")) + .build(); + try { + Utils.indexADocument(SEARCH_INDEX, document); + } catch (InterruptedException e) { + // ignore + } + PrintWriter out = resp.getWriter(); + Results result = doSearch(); + for (ScoredDocument doc : result.getResults()) { + out.println(doc.toString()); + } + } +} diff --git a/appengine-java8/search/src/main/java/com/example/appengine/search/SearchServlet.java b/appengine-java8/search/src/main/java/com/example/appengine/search/SearchServlet.java new file mode 100644 index 00000000000..e92005b55a6 --- /dev/null +++ b/appengine-java8/search/src/main/java/com/example/appengine/search/SearchServlet.java @@ -0,0 +1,117 @@ +/** + * Copyright 2016 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.example.appengine.search; + +// @formatter:off +// [START search_document_import] +import com.google.appengine.api.search.Document; +import com.google.appengine.api.search.Field; +import com.google.appengine.api.search.Results; +import com.google.appengine.api.search.ScoredDocument; +import com.google.appengine.api.search.SearchException; +import com.google.appengine.api.search.StatusCode; +// [END search_document_import] + +// CHECKSTYLE:OFF +import com.google.appengine.api.search.Index; +import com.google.appengine.api.search.IndexSpec; +import com.google.appengine.api.search.SearchServiceFactory; +// @formatter:on +// CHECKSTYLE:ON + +import java.io.IOException; +import java.io.PrintWriter; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + + +@SuppressWarnings("serial") +public class SearchServlet extends HttpServlet { + + private static final String SEARCH_INDEX = "searchIndex"; + + private Index getIndex() { + IndexSpec indexSpec = IndexSpec.newBuilder().setName(SEARCH_INDEX).build(); + Index index = SearchServiceFactory.getSearchService().getIndex(indexSpec); + return index; + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + PrintWriter out = resp.getWriter(); + Document doc = Document.newBuilder() + .setId("theOnlyPiano") + .addField(Field.newBuilder().setName("product").setText("piano")) + .addField(Field.newBuilder().setName("maker").setText("Yamaha")) + .addField(Field.newBuilder().setName("price").setNumber(4000)) + .build(); + try { + Utils.indexADocument(SEARCH_INDEX, doc); + } catch (InterruptedException e) { + // ignore + } + // [START search_document] + final int maxRetry = 3; + int attempts = 0; + int delay = 2; + while (true) { + try { + String queryString = "product = piano AND price < 5000"; + Results results = getIndex().search(queryString); + + // Iterate over the documents in the results + for (ScoredDocument document : results) { + // handle results + out.print("maker: " + document.getOnlyField("maker").getText()); + out.println(", price: " + document.getOnlyField("price").getNumber()); + } + } catch (SearchException e) { + if (StatusCode.TRANSIENT_ERROR.equals(e.getOperationResult().getCode()) + && ++attempts < maxRetry) { + // retry + try { + Thread.sleep(delay * 1000); + } catch (InterruptedException e1) { + // ignore + } + delay *= 2; // easy exponential backoff + continue; + } else { + throw e; + } + } + break; + } + // [END search_document] + // We don't test the search result below, but we're fine if it runs without errors. + out.println("Search performed"); + Index index = getIndex(); + // [START simple_search_1] + index.search("rose water"); + // [END simple_search_1] + // [START simple_search_2] + index.search("1776-07-04"); + // [END simple_search_2] + // [START simple_search_3] + // search for documents with pianos that cost less than $5000 + index.search("product = piano AND price < 5000"); + // [END simple_search_3] + } +} diff --git a/appengine-java8/search/src/main/java/com/example/appengine/search/Utils.java b/appengine-java8/search/src/main/java/com/example/appengine/search/Utils.java new file mode 100644 index 00000000000..4fab0159303 --- /dev/null +++ b/appengine-java8/search/src/main/java/com/example/appengine/search/Utils.java @@ -0,0 +1,63 @@ +/** + * Copyright 2016 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.example.appengine.search; + +// [START index_import] +import com.google.appengine.api.search.Document; +import com.google.appengine.api.search.Index; +import com.google.appengine.api.search.IndexSpec; +import com.google.appengine.api.search.PutException; +import com.google.appengine.api.search.SearchServiceFactory; +import com.google.appengine.api.search.StatusCode; +// [END index_import] + +/** + * A utility class for the search API sample. + */ +public class Utils { + /** + * Put a given document into an index with the given indexName. + * @param indexName The name of the index. + * @param document A document to add. + * @throws InterruptedException When Thread.sleep is interrupted. + */ + // [START putting_document_with_retry] + public static void indexADocument(String indexName, Document document) + throws InterruptedException { + IndexSpec indexSpec = IndexSpec.newBuilder().setName(indexName).build(); + Index index = SearchServiceFactory.getSearchService().getIndex(indexSpec); + + final int maxRetry = 3; + int attempts = 0; + int delay = 2; + while (true) { + try { + index.put(document); + } catch (PutException e) { + if (StatusCode.TRANSIENT_ERROR.equals(e.getOperationResult().getCode()) + && ++attempts < maxRetry) { // retrying + Thread.sleep(delay * 1000); + delay *= 2; // easy exponential backoff + continue; + } else { + throw e; // otherwise throw + } + } + break; + } + } + // [END putting_document_with_retry] +} diff --git a/appengine-java8/search/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/search/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..d64ab6aafb7 --- /dev/null +++ b/appengine-java8/search/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,19 @@ + + + + + + java8 + true + diff --git a/appengine-java8/search/src/main/webapp/WEB-INF/web.xml b/appengine-java8/search/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..d3e1e6f8ec3 --- /dev/null +++ b/appengine-java8/search/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,54 @@ + + + + document + com.example.appengine.search.DocumentServlet + + + document + / + + + index + com.example.appengine.search.IndexServlet + + + index + /index + + + search + com.example.appengine.search.SearchServlet + + + search + /search + + + search_option + com.example.appengine.search.SearchOptionServlet + + + search_option + /search_option + + + delete + com.example.appengine.search.DeleteServlet + + + delete + /delete + + + schema + com.example.appengine.search.SchemaServlet + + + schema + /schema + + diff --git a/appengine-java8/search/src/test/java/com/example/appengine/search/DeleteServletTest.java b/appengine-java8/search/src/test/java/com/example/appengine/search/DeleteServletTest.java new file mode 100644 index 00000000000..15b9deec1fb --- /dev/null +++ b/appengine-java8/search/src/test/java/com/example/appengine/search/DeleteServletTest.java @@ -0,0 +1,66 @@ +/** + * Copyright 2016 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.example.appengine.search; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class DeleteServletTest { + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private DeleteServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new DeleteServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_successfulyInvoked() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + assertThat(responseWriter.toString()) + .named("DeleteServlet response") + .contains("Deleted documents."); + } +} \ No newline at end of file diff --git a/appengine-java8/search/src/test/java/com/example/appengine/search/DocumentServletTest.java b/appengine-java8/search/src/test/java/com/example/appengine/search/DocumentServletTest.java new file mode 100644 index 00000000000..55bb6ed5350 --- /dev/null +++ b/appengine-java8/search/src/test/java/com/example/appengine/search/DocumentServletTest.java @@ -0,0 +1,105 @@ +/** + * Copyright 2016 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.example.appengine.search; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.api.search.Document; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class DocumentServletTest { + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private DocumentServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new DocumentServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_successfulyInvoked() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + String content = responseWriter.toString(); + assertThat(content) + .named("DocumentServlet response: coverLetter") + .contains("coverLetter: CoverLetter"); + assertThat(content) + .named("DocumentServlet response: resume") + .contains("resume: "); + assertThat(content) + .named("DocumentServlet response: fullName") + .contains("fullName: Foo Bar"); + assertThat(content) + .named("DocumentServlet response: submissionDate") + .contains("submissionDate: "); + } + + @Test + public void createDocument_withSignedInUser() throws Exception { + String email = "tmatsuo@example.com"; + String authDomain = "example.com"; + helper.setEnvEmail(email); + helper.setEnvAuthDomain(authDomain); + helper.setEnvIsLoggedIn(true); + Document doc = servletUnderTest.createDocument(); + assertThat(doc.getOnlyField("content").getText()) + .named("content") + .contains("the rain in spain"); + assertThat(doc.getOnlyField("email").getText()) + .named("email") + .isEqualTo(email); + } + + @Test + public void createDocument_withoutSignedIn() throws Exception { + helper.setEnvIsLoggedIn(false); + Document doc = servletUnderTest.createDocument(); + assertThat(doc.getOnlyField("content").getText()) + .named("content") + .contains("the rain in spain"); + assertThat(doc.getOnlyField("email").getText()) + .named("email") + .isEmpty(); + } +} \ No newline at end of file diff --git a/appengine-java8/search/src/test/java/com/example/appengine/search/IndexServletTest.java b/appengine-java8/search/src/test/java/com/example/appengine/search/IndexServletTest.java new file mode 100644 index 00000000000..ce3a3eee55c --- /dev/null +++ b/appengine-java8/search/src/test/java/com/example/appengine/search/IndexServletTest.java @@ -0,0 +1,66 @@ +/** + * Copyright 2016 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.example.appengine.search; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class IndexServletTest { + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private IndexServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new IndexServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_successfulyInvoked() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + assertThat(responseWriter.toString()) + .named("IndexServlet response") + .contains("myField: myValue"); + } +} \ No newline at end of file diff --git a/appengine-java8/search/src/test/java/com/example/appengine/search/SchemaServletTest.java b/appengine-java8/search/src/test/java/com/example/appengine/search/SchemaServletTest.java new file mode 100644 index 00000000000..c5c459c55fa --- /dev/null +++ b/appengine-java8/search/src/test/java/com/example/appengine/search/SchemaServletTest.java @@ -0,0 +1,76 @@ +/** + * Copyright 2016 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.example.appengine.search; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class SchemaServletTest { + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private SchemaServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new SchemaServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_successfulyInvoked() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + String content = responseWriter.toString(); + assertThat(content) + .named("SchemaServlet response") + .contains("schemaIndex:maker:TEXT"); + assertThat(content) + .named("SchemaServlet response") + .contains("schemaIndex:price:NUMBER"); + assertThat(content) + .named("SchemaServlet response") + .contains("schemaIndex:color:TEXT"); + assertThat(content) + .named("SchemaServlet response") + .contains("schemaIndex:model:TEXT"); + } +} \ No newline at end of file diff --git a/appengine-java8/search/src/test/java/com/example/appengine/search/SearchOptionServletTest.java b/appengine-java8/search/src/test/java/com/example/appengine/search/SearchOptionServletTest.java new file mode 100644 index 00000000000..ae055868e9e --- /dev/null +++ b/appengine-java8/search/src/test/java/com/example/appengine/search/SearchOptionServletTest.java @@ -0,0 +1,66 @@ +/** + * Copyright 2016 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.example.appengine.search; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class SearchOptionServletTest { + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private SearchOptionServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new SearchOptionServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_successfulyInvoked() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + assertThat(responseWriter.toString()) + .named("SearchOptionServlet response") + .contains("documentId=theOnlyCoffeeRoaster"); + } +} diff --git a/appengine-java8/search/src/test/java/com/example/appengine/search/SearchServletTest.java b/appengine-java8/search/src/test/java/com/example/appengine/search/SearchServletTest.java new file mode 100644 index 00000000000..a405b5a8693 --- /dev/null +++ b/appengine-java8/search/src/test/java/com/example/appengine/search/SearchServletTest.java @@ -0,0 +1,70 @@ +/** + * Copyright 2016 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.example.appengine.search; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class SearchServletTest { + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequest; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private SearchServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new SearchServlet(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_successfulyInvoked() throws Exception { + servletUnderTest.doGet(mockRequest, mockResponse); + String content = responseWriter.toString(); + assertThat(content) + .named("SearchServlet response") + .contains("maker: Yamaha"); + assertThat(content) + .named("SearchServlet response") + .contains("price: 4000.0"); + } +} \ No newline at end of file diff --git a/appengine-java8/search/src/test/java/com/example/appengine/search/UtilsTest.java b/appengine-java8/search/src/test/java/com/example/appengine/search/UtilsTest.java new file mode 100644 index 00000000000..e3a3e58cf2a --- /dev/null +++ b/appengine-java8/search/src/test/java/com/example/appengine/search/UtilsTest.java @@ -0,0 +1,62 @@ +/** + * Copyright 2016 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.example.appengine.search; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.appengine.api.search.Document; +import com.google.appengine.api.search.Field; +import com.google.appengine.api.search.Index; +import com.google.appengine.api.search.IndexSpec; +import com.google.appengine.api.search.SearchServiceFactory; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + + +public class UtilsTest { + private static final String INDEX = "UtilsTestIndex"; + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Before + public void setUp() throws Exception { + helper.setUp(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void indexADocument_successfullyInvoked() throws Exception { + String id = "test"; + Document doc = Document.newBuilder() + .setId(id) + .addField(Field.newBuilder().setName("f").setText("v")) + .build(); + Utils.indexADocument(INDEX, doc); + // get the document by id + IndexSpec indexSpec = IndexSpec.newBuilder().setName(INDEX).build(); + Index index = SearchServiceFactory.getSearchService().getIndex(indexSpec); + Document fetched = index.get(id); + assertThat(fetched.getOnlyField("f").getText()) + .named("A value of the fetched document") + .isEqualTo("v"); + } +} \ No newline at end of file diff --git a/appengine-java8/sendgrid/README.md b/appengine-java8/sendgrid/README.md new file mode 100644 index 00000000000..d7a8f6b0654 --- /dev/null +++ b/appengine-java8/sendgrid/README.md @@ -0,0 +1,32 @@ +# Java SendGrid Email Sample for Google App Engine Standard Environment + +This sample demonstrates how to use [SendGrid](https://www.sendgrid.com) on +[Google App Engine standard environment][ae-docs]. + +See the [sample application documentaion][sample-docs] for more detailed +instructions. + +For more information about SendGrid, see their +[documentation](https://sendgrid.com/docs/User_Guide/index.html). + +[ae-docs]: https://cloud.google.com/appengine/docs/java/ +[sample-docs]: https://cloud.google.com/appengine/docs/java/mail/sendgrid + +## Setup and deploy + +Before you can run or deploy the sample, you will need to do the following: + +1. [Create a SendGrid Account](http://sendgrid.com/partner/google). As of + September 2015, Google users start with 25,000 free emails per month. +1. Configure your SendGrid settings in the environment variables section in + [`src/main/webapp/WEB-INF/appengine-web.xml`](src/main/webapp/WEB-INF/appengine-web.xml). +1. Visit /send/email?to=YOUR-EMAIL-ADDRESS + +## Running locally + +You can run the application locally and send emails from your local machine. You +will need to set environment variables before starting your application: + + export SENDGRID_API_KEY=[your-sendgrid-api-key] + export SENDGRID_SENDER=[your-sendgrid-sender-email-address] + mvn clean appengine:run diff --git a/appengine-java8/sendgrid/pom.xml b/appengine-java8/sendgrid/pom.xml new file mode 100644 index 00000000000..64b2c60c592 --- /dev/null +++ b/appengine-java8/sendgrid/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-sendgrid-j8 + + + appengine-java8-samples + com.google.cloud + 1.0.0 + .. + + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + com.sendgrid + sendgrid-java + 2.2.2 + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/sendgrid/src/main/java/com/example/appengine/sendgrid/SendEmailServlet.java b/appengine-java8/sendgrid/src/main/java/com/example/appengine/sendgrid/SendEmailServlet.java new file mode 100644 index 00000000000..db086c739eb --- /dev/null +++ b/appengine-java8/sendgrid/src/main/java/com/example/appengine/sendgrid/SendEmailServlet.java @@ -0,0 +1,64 @@ +/** + * Copyright 2015 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.example.appengine.sendgrid; + +import com.sendgrid.SendGrid; +import com.sendgrid.SendGridException; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class SendEmailServlet extends HttpServlet { + + @Override + public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + final String sendgridApiKey = System.getenv("SENDGRID_API_KEY"); + final String sendgridSender = System.getenv("SENDGRID_SENDER"); + final String toEmail = req.getParameter("to"); + if (toEmail == null) { + resp.getWriter() + .print("Please provide an email address in the \"to\" query string parameter."); + return; + } + + SendGrid sendgrid = new SendGrid(sendgridApiKey); + SendGrid.Email email = new SendGrid.Email(); + email.addTo(toEmail); + email.setFrom(sendgridSender); + email.setSubject("This is a test email"); + email.setText("Example text body."); + + try { + SendGrid.Response response = sendgrid.send(email); + if (response.getCode() != 200) { + resp.getWriter().print(String.format("An error occured: %s", response.getMessage())); + return; + } + resp.getWriter().print("Email sent."); + } catch (SendGridException e) { + throw new ServletException("SendGrid error", e); + } + } +} +// [END example] diff --git a/appengine-java8/sendgrid/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/sendgrid/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..2573bc11ab1 --- /dev/null +++ b/appengine-java8/sendgrid/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,24 @@ + + + + + + java8 + true + + + + + + diff --git a/appengine-java8/sendgrid/src/main/webapp/WEB-INF/web.xml b/appengine-java8/sendgrid/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..23a0e83b821 --- /dev/null +++ b/appengine-java8/sendgrid/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,29 @@ + + + + + + + sendemail + com.example.appengine.sendgrid.SendEmailServlet + + + sendemail + /send/email + + + diff --git a/appengine-java8/static-files/pom.xml b/appengine-java8/static-files/pom.xml new file mode 100644 index 00000000000..1d4d229cf39 --- /dev/null +++ b/appengine-java8/static-files/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-staticfiles-j8 + + + appengine-java8-samples + com.google.cloud + 1.0.0 + .. + + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/static-files/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/static-files/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..170e6bbe177 --- /dev/null +++ b/appengine-java8/static-files/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,20 @@ + + + + + + java8 + true + + diff --git a/appengine-java8/static-files/src/main/webapp/WEB-INF/web.xml b/appengine-java8/static-files/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..4d16ff09139 --- /dev/null +++ b/appengine-java8/static-files/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,24 @@ + + + + + + + index.html + + + diff --git a/appengine-java8/static-files/src/main/webapp/index.html b/appengine-java8/static-files/src/main/webapp/index.html new file mode 100644 index 00000000000..d1643e9a6b1 --- /dev/null +++ b/appengine-java8/static-files/src/main/webapp/index.html @@ -0,0 +1,10 @@ + + + +Static Files + + + +

This is a static file serving example.

+ + diff --git a/appengine-java8/static-files/src/main/webapp/stylesheets/styles.css b/appengine-java8/static-files/src/main/webapp/stylesheets/styles.css new file mode 100644 index 00000000000..573f441093f --- /dev/null +++ b/appengine-java8/static-files/src/main/webapp/stylesheets/styles.css @@ -0,0 +1,4 @@ +body { + font-family: Verdana, Helvetica, sans-serif; + background-color: #CCCCFF; +} diff --git a/appengine-java8/taskqueue/README.md b/appengine-java8/taskqueue/README.md new file mode 100644 index 00000000000..a8cda57b7a3 --- /dev/null +++ b/appengine-java8/taskqueue/README.md @@ -0,0 +1,5 @@ +# Task Queue Java Snippets + +These are Java samples for using the [Task Queue](https://cloud.google.com/appengine/docs/java/taskqueue/) + + diff --git a/appengine-java8/taskqueue/deferred/README.md b/appengine-java8/taskqueue/deferred/README.md new file mode 100644 index 00000000000..8ed90310827 --- /dev/null +++ b/appengine-java8/taskqueue/deferred/README.md @@ -0,0 +1,24 @@ +App Engine Java Guestbook +Copyright (C) 2010-2012 Google Inc. + +## Sample guestbook for use with App Engine Java. + +Requires [Apache Maven](http://maven.apache.org) 3.1 or greater, and JDK 7+ in order to run. + +To build, run + + mvn package + +Building will run the tests, but to explicitly run tests you can use the test target + + mvn test + +To start the app, use the [App Engine Maven Plugin](http://code.google.com/p/appengine-maven-plugin/) that is already included in this demo. Just run the command. + + mvn appengine:devserver + +For further information, consult the [Java App Engine](https://developers.google.com/appengine/docs/java/overview) documentation. + +To see all the available goals for the App Engine plugin, run + + mvn help:describe -Dplugin=appengine \ No newline at end of file diff --git a/appengine-java8/taskqueue/deferred/pom.xml b/appengine-java8/taskqueue/deferred/pom.xml new file mode 100644 index 00000000000..13254ab6c1a --- /dev/null +++ b/appengine-java8/taskqueue/deferred/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.google.cloud.taskqueue.samples + taskqueue-defer-j8 + + + + appengine-java8-samples + com.google.cloud + 1.0.0 + ../.. + + + + 1.8 + 1.8 + 1.9.52 + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + jstl + jstl + 1.2 + + + + + junit + junit + 4.12 + + + org.mockito + mockito-core + 2.7.22 + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + + diff --git a/appengine-java8/taskqueue/deferred/src/main/java/com/google/cloud/taskqueue/samples/DeferSampleServlet.java b/appengine-java8/taskqueue/deferred/src/main/java/com/google/cloud/taskqueue/samples/DeferSampleServlet.java new file mode 100644 index 00000000000..82e9057f2da --- /dev/null +++ b/appengine-java8/taskqueue/deferred/src/main/java/com/google/cloud/taskqueue/samples/DeferSampleServlet.java @@ -0,0 +1,76 @@ +/** + * Copyright 2015 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. + */ + +/** + * This package demonstrates how to use the task queue with Java. + */ +package com.google.cloud.taskqueue.samples; + +import com.google.appengine.api.taskqueue.DeferredTask; +import com.google.appengine.api.taskqueue.Queue; +import com.google.appengine.api.taskqueue.QueueFactory; +import com.google.appengine.api.taskqueue.TaskOptions; + +import java.io.IOException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * This small servlet demonstrates how to use the DeferredTask + * interface to background a task on the AppEngine task queues, + * without needing to create a separate URL handler. + */ +public class DeferSampleServlet extends HttpServlet { + + /** + * Number of ms long we will arbitrarily delay. + */ + static final int DELAY_MS = 5000; + + //[START defer] + /** + * A hypothetical expensive operation we want to defer on a background task. + */ + public static class ExpensiveOperation implements DeferredTask { + @Override + public void run() { + System.out.println("Doing an expensive operation..."); + // expensive operation to be backgrounded goes here + } + } + + /** + * Basic demonstration of adding a deferred task. + * @param request servlet request + * @param resp servlet response + */ + @Override + public void doGet(final HttpServletRequest request, + final HttpServletResponse resp) throws IOException { + // Add the task to the default queue. + Queue queue = QueueFactory.getDefaultQueue(); + + // Wait 5 seconds to run for demonstration purposes + queue.add(TaskOptions.Builder.withPayload(new ExpensiveOperation()) + .etaMillis(System.currentTimeMillis() + DELAY_MS)); + + resp.setContentType("text/plain"); + resp.getWriter().println("Task is backgrounded on queue!"); + } + //[END defer] + +} diff --git a/appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..6ced9a90b72 --- /dev/null +++ b/appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,25 @@ + + + + + java8 + true + + + + + diff --git a/appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..0c2ea51bc6d --- /dev/null +++ b/appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,13 @@ +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# + +# Set the default logging level for all loggers to WARNING +.level = WARNING diff --git a/appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/web.xml b/appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..a8b724b5871 --- /dev/null +++ b/appengine-java8/taskqueue/deferred/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,30 @@ + + + + + defer-sample-servlet + com.google.cloud.taskqueue.samples.DeferSampleServlet + + + defer-sample-servlet + /defer + + diff --git a/appengine-java8/taskqueue/deferred/src/main/webapp/guestbook.jsp b/appengine-java8/taskqueue/deferred/src/main/webapp/guestbook.jsp new file mode 100644 index 00000000000..52530b8fef0 --- /dev/null +++ b/appengine-java8/taskqueue/deferred/src/main/webapp/guestbook.jsp @@ -0,0 +1,110 @@ +<%-- + Copyright 2015 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. + +--%> + +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="com.google.appengine.api.datastore.DatastoreService" %> +<%@ page import="com.google.appengine.api.datastore.DatastoreServiceFactory" %> +<%@ page import="com.google.appengine.api.datastore.Entity" %> +<%@ page import="com.google.appengine.api.datastore.FetchOptions" %> +<%@ page import="com.google.appengine.api.datastore.Key" %> +<%@ page import="com.google.appengine.api.datastore.KeyFactory" %> +<%@ page import="com.google.appengine.api.datastore.Query" %> +<%@ page import="com.google.appengine.api.users.User" %> +<%@ page import="com.google.appengine.api.users.UserService" %> +<%@ page import="com.google.appengine.api.users.UserServiceFactory" %> +<%@ page import="java.util.List" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + + + + + + + + +<% + String guestbookName = request.getParameter("guestbookName"); + if (guestbookName == null) { + guestbookName = "default"; + } + pageContext.setAttribute("guestbookName", guestbookName); + UserService userService = UserServiceFactory.getUserService(); + User user = userService.getCurrentUser(); + if (user != null) { + pageContext.setAttribute("user", user); +%> +

Hello, ${fn:escapeXml(user.nickname)}! (You can + sign out.)

+<% +} else { +%> +

Hello! + Sign in + to include your name with greetings you post.

+<% + } +%> + +<% + DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName); + // Run an ancestor query to ensure we see the most up-to-date + // view of the Greetings belonging to the selected Guestbook. + Query query = new Query("Greeting", guestbookKey).addSort("date", Query.SortDirection.DESCENDING); + List greetings = datastore.prepare(query).asList(FetchOptions.Builder.withLimit(5)); + if (greetings.isEmpty()) { +%> +

Guestbook '${fn:escapeXml(guestbookName)}' has no messages.

+<% +} else { +%> +

Messages in Guestbook '${fn:escapeXml(guestbookName)}'.

+<% + for (Entity greeting : greetings) { + pageContext.setAttribute("greeting_content", + greeting.getProperty("content")); + if (greeting.getProperty("user") == null) { +%> +

An anonymous person wrote:

+<% +} else { + pageContext.setAttribute("greeting_user", + greeting.getProperty("user")); +%> +

${fn:escapeXml(greeting_user.nickname)} wrote:

+<% + } +%> +
${fn:escapeXml(greeting_content)}
+<% + } + } +%> + +
+
+
+ +
+ +
+
+
+
+ + + diff --git a/appengine-java8/taskqueue/pull/README.md b/appengine-java8/taskqueue/pull/README.md new file mode 100644 index 00000000000..0f51bfc1cf4 --- /dev/null +++ b/appengine-java8/taskqueue/pull/README.md @@ -0,0 +1,23 @@ +# Pull Task Queue sample for Google App Engine + +This sample demonstrates how to use [pull task queues][appid] on [Google App +Engine][ae-docs]. + +[appid]: https://cloud.google.com/appengine/docs/java/taskqueue/overview-pull +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Setup + + gcloud init + +## Running locally +This example uses the +[Cloud SDK based maven plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven). +To run this sample locally: + + mvn appengine:run + +## Deploying + + mvn appengine:deploy + diff --git a/appengine-java8/taskqueue/pull/pom.xml b/appengine-java8/taskqueue/pull/pom.xml new file mode 100644 index 00000000000..68c6ed7fae0 --- /dev/null +++ b/appengine-java8/taskqueue/pull/pom.xml @@ -0,0 +1,80 @@ + + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.taskqueue + appengine-taskqueue-j8 + + + appengine-java8-samples + com.google.cloud + 1.0.0 + ../.. + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + jstl + jstl + 1.2 + + + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/taskqueue/pull/src/main/java/com/example/taskqueue/TaskqueueServlet.java b/appengine-java8/taskqueue/pull/src/main/java/com/example/taskqueue/TaskqueueServlet.java new file mode 100644 index 00000000000..35c57ad4512 --- /dev/null +++ b/appengine-java8/taskqueue/pull/src/main/java/com/example/taskqueue/TaskqueueServlet.java @@ -0,0 +1,135 @@ +/** + * Copyright 2015 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.example.taskqueue; + +import com.google.appengine.api.taskqueue.Queue; +import com.google.appengine.api.taskqueue.QueueFactory; +import com.google.appengine.api.taskqueue.TaskHandle; +import com.google.appengine.api.taskqueue.TaskOptions; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Form Handling Servlet -- takes the form submission from /src/main/webapp/tasks.jsp to add and + * delete tasks. + */ +public class TaskqueueServlet extends HttpServlet { + private static final Logger log = Logger.getLogger(TaskqueueServlet.class.getName()); + private static final int numberOfTasksToAdd = 100; + private static final int numberOfTasksToLease = 100; + private static boolean useTaggedTasks = true; + private static String output; + private static String message; + + // Process the http POST of the form + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + if (req.getParameter("addTask") != null) { + String content = req.getParameter("content"); + String output = String.format("Adding %d Tasks to the Task Queue with a payload of '%s'", + numberOfTasksToAdd, content.toString()); + log.info(output.toString()); + + // Add Tasks to Task Queue + // [START get_queue] + Queue q = QueueFactory.getQueue("pull-queue"); + // [END get_queue] + if (!useTaggedTasks) { + for (int i = 0; i < numberOfTasksToAdd; i++) { + // [START add_task] + q.add(TaskOptions.Builder.withMethod(TaskOptions.Method.PULL) + .payload(content.toString())); + // [END add_task] + } + } else { + for (int i = 0; i < numberOfTasksToAdd; i++) { + // [START add_task_w_tag] + q.add(TaskOptions.Builder.withMethod(TaskOptions.Method.PULL) + .payload(content.toString()) + .tag("process".getBytes())); + // [END add_task_w_tag] + } + } + try { + message = "Added " + numberOfTasksToAdd + " tasks to the task queue."; + req.setAttribute("message", message); + req.getRequestDispatcher("tasks.jsp").forward(req,resp); + } catch (ServletException e) { + throw new ServletException("ServletException error: ", e); + } + } else { + if (req.getParameter("leaseTask") != null) { + output = String.format("Pulling %d Tasks from the Task Queue", numberOfTasksToLease); + log.info(output.toString()); + + // Pull tasks from the Task Queue and process them + Queue q = QueueFactory.getQueue("pull-queue"); + if (!useTaggedTasks) { + // [START lease_tasks] + List tasks = q.leaseTasks(3600, TimeUnit.SECONDS, numberOfTasksToLease); + // [END lease_tasks] + message = processTasks(tasks, q); + } else { + // [START lease_tasks_by_tag] + // Lease only tasks tagged with "process" + List tasks = q.leaseTasksByTag(3600, TimeUnit.SECONDS, numberOfTasksToLease, + "process"); + // You can also specify a tag to lease via LeaseOptions passed to leaseTasks. + // [END lease_tasks_by_tag] + message = processTasks(tasks, q); + } + req.setAttribute("message", message); + req.getRequestDispatcher("tasks.jsp").forward(req,resp); + } else { + resp.sendRedirect("/"); + } + } + } + + //Method to process and delete tasks + private static String processTasks(List tasks, Queue q) { + String payload; + int numberOfDeletedTasks = 0; + for (TaskHandle task : tasks) { + payload = new String(task.getPayload()); + output = String.format("Processing: taskName='%s' payload='%s'", task.getName() + .toString(), payload.toString()); + log.info(output.toString()); + output = String.format("Deleting taskName='%s'", task.getName().toString()); + log.info(output.toString()); + // [START delete_task] + q.deleteTask(task); + // [END delete_task] + numberOfDeletedTasks++; + } + if (numberOfDeletedTasks > 0) { + message = "Processed and deleted " + numberOfTasksToLease + " tasks from the " + + " task queue."; + } else { + message = "Task Queue has no tasks available for lease."; + } + return message; + } +} diff --git a/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..d64ab6aafb7 --- /dev/null +++ b/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,19 @@ + + + + + + java8 + true + diff --git a/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..b2d2797cada --- /dev/null +++ b/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,27 @@ +# Copyright 2016 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. + +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# + +# Set the default logging level for all loggers to WARNING +.level = INFO diff --git a/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/queue.xml b/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/queue.xml new file mode 100644 index 00000000000..25833c701d8 --- /dev/null +++ b/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/queue.xml @@ -0,0 +1,11 @@ + + + pull-queue + pull + + bar@foo.com + user@gmail.com + bar@foo.com + + + diff --git a/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/web.xml b/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..807f08387c2 --- /dev/null +++ b/appengine-java8/taskqueue/pull/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,33 @@ + + + + + + + taskqueue + com.example.taskqueue.TaskqueueServlet + + + + taskqueue + /taskqueue + + + + tasks.jsp + + diff --git a/appengine-java8/taskqueue/pull/src/main/webapp/tasks.jsp b/appengine-java8/taskqueue/pull/src/main/webapp/tasks.jsp new file mode 100644 index 00000000000..02f12a901dc --- /dev/null +++ b/appengine-java8/taskqueue/pull/src/main/webapp/tasks.jsp @@ -0,0 +1,50 @@ + + +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="java.util.List" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + + + + + + + + +
+
+
+
+
+
+
+
+
+
+ + + + diff --git a/appengine-java8/taskqueue/push/README.md b/appengine-java8/taskqueue/push/README.md new file mode 100644 index 00000000000..e235666bdac --- /dev/null +++ b/appengine-java8/taskqueue/push/README.md @@ -0,0 +1,25 @@ +# A Java Task Queue example for Google App Engine + +This sample demonstrates how to use the [TaskQueue API][taskqueue-api] on [Google App +Engine][ae-docs]. + +[taskqueue-api]: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/taskqueue/package-summary +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Setup + + gcloud init + +## Running locally +This example uses the +[Maven gcloud plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven). +To run this sample locally: + + mvn appengine:run + +Go to the site `localhost:8080` to add elements to the queue. They will appear in the log as the result of the Enqueue servlet transmitting the data to the Worker servlet. + +## Deploying + + mvn appengine:deploy + diff --git a/appengine-java8/taskqueue/push/pom.xml b/appengine-java8/taskqueue/push/pom.xml new file mode 100644 index 00000000000..7274ac04056 --- /dev/null +++ b/appengine-java8/taskqueue/push/pom.xml @@ -0,0 +1,103 @@ + + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + taskqueue-push-j8 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + ../.. + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + org.json + json + 20160810 + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.32 + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/taskqueue/push/src/main/java/com/example/appengine/taskqueue/push/Enqueue.java b/appengine-java8/taskqueue/push/src/main/java/com/example/appengine/taskqueue/push/Enqueue.java new file mode 100644 index 00000000000..e0fbeaac062 --- /dev/null +++ b/appengine-java8/taskqueue/push/src/main/java/com/example/appengine/taskqueue/push/Enqueue.java @@ -0,0 +1,42 @@ +/* Copyright 2016 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.example.appengine.taskqueue.push; + +import com.google.appengine.api.taskqueue.Queue; +import com.google.appengine.api.taskqueue.QueueFactory; +import com.google.appengine.api.taskqueue.TaskOptions; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START enqueue] +// The Enqueue servlet should be mapped to the "/enqueue" URL. +public class Enqueue extends HttpServlet { + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String key = request.getParameter("key"); + + // Add the task to the default queue. + Queue queue = QueueFactory.getDefaultQueue(); + queue.add(TaskOptions.Builder.withUrl("/worker").param("key", key)); + + response.sendRedirect("/"); + } +} +// [END enqueue] diff --git a/appengine-java8/taskqueue/push/src/main/java/com/example/appengine/taskqueue/push/Worker.java b/appengine-java8/taskqueue/push/src/main/java/com/example/appengine/taskqueue/push/Worker.java new file mode 100644 index 00000000000..63c42af1d1d --- /dev/null +++ b/appengine-java8/taskqueue/push/src/main/java/com/example/appengine/taskqueue/push/Worker.java @@ -0,0 +1,40 @@ +/* Copyright 2016 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.example.appengine.taskqueue.push; + +import java.io.IOException; +import java.util.logging.Logger; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START worker] +// The Worker servlet should be mapped to the "/worker" URL. +public class Worker extends HttpServlet { + private static final Logger log = Logger.getLogger(Worker.class.getName()); + + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String key = request.getParameter("key"); + + // Do something with key. + // [START_EXCLUDE] + log.info("Worker is processing " + key); + // [END_EXCLUDE] + } +} +// [END worker] diff --git a/appengine-java8/taskqueue/push/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/taskqueue/push/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..f264bd56e46 --- /dev/null +++ b/appengine-java8/taskqueue/push/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,20 @@ + + + + java8 + true + diff --git a/appengine-java8/taskqueue/push/src/main/webapp/WEB-INF/web.xml b/appengine-java8/taskqueue/push/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..b1ab8458b5b --- /dev/null +++ b/appengine-java8/taskqueue/push/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,39 @@ + + + + + index.html + + + enqueue + com.example.appengine.taskqueue.push.Enqueue + + + worker + com.example.appengine.taskqueue.push.Worker + + + enqueue + /enqueue + + + worker + /worker + + diff --git a/appengine-java8/taskqueue/push/src/main/webapp/index.html b/appengine-java8/taskqueue/push/src/main/webapp/index.html new file mode 100644 index 00000000000..edec84176f0 --- /dev/null +++ b/appengine-java8/taskqueue/push/src/main/webapp/index.html @@ -0,0 +1,28 @@ + + + + + + +

Enqueue a value, to be processed by a worker.

+
+ + +
+ + + diff --git a/appengine-java8/taskqueue/push/src/test/java/com/example/appengine/taskqueue/push/WorkerTest.java b/appengine-java8/taskqueue/push/src/test/java/com/example/appengine/taskqueue/push/WorkerTest.java new file mode 100644 index 00000000000..2d82e3ee69d --- /dev/null +++ b/appengine-java8/taskqueue/push/src/test/java/com/example/appengine/taskqueue/push/WorkerTest.java @@ -0,0 +1,79 @@ +/* Copyright 2016 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.example.appengine.taskqueue.push; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link Worker}. + */ +@RunWith(JUnit4.class) +public class WorkerTest { + private static final String FAKE_KEY_VALUE = "KEY"; + + // To capture and restore stderr + private final ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + private static final PrintStream REAL_ERR = System.err; + + @Mock + private HttpServletRequest mockRequest; + @Mock + private HttpServletResponse mockResponse; + private Worker servletUnderTest; + + @Before + public void setUp() throws Exception { + // Capture stderr to examine messages written to it + System.setErr(new PrintStream(stderr)); + + MockitoAnnotations.initMocks(this); + + when(mockRequest.getParameter("key")).thenReturn(FAKE_KEY_VALUE); + + servletUnderTest = new Worker(); + } + + @After + public void tearDown() { + // Restore stderr + System.setErr(WorkerTest.REAL_ERR); + } + + @Test + public void doPost_writesResponse() throws Exception { + servletUnderTest.doPost(mockRequest, mockResponse); + + String out = stderr.toString(); + // We expect a log message to be created + // with the following message. + assertThat(out).contains("Worker is processing " + FAKE_KEY_VALUE); + + } +} diff --git a/appengine-java8/twilio/README.md b/appengine-java8/twilio/README.md new file mode 100644 index 00000000000..16c71862da1 --- /dev/null +++ b/appengine-java8/twilio/README.md @@ -0,0 +1,37 @@ +# Java Twilio Voice and SMS Sample for Google App Engine Standard Environment + +This sample demonstrates how to use [Twilio](https://www.twilio.com) on [Google +App Engine standard environment][ae-docs]. + +See the [sample application documentaion][sample-docs] for more detailed +instructions. + +For more information about Twilio, see their [Java quickstart +tutorials](https://www.twilio.com/docs/quickstart/java). + +[ae-docs]: https://cloud.google.com/appengine/docs/java/ +[sample-docs]: https://cloud.google.com/appengine/docs/java/sms/twilio + + +## Setup + +Before you can run or deploy the sample, you will need to do the following: + +1. [Create a Twilio Account](http://ahoy.twilio.com/googlecloudplatform). Google + App Engine customers receive a complimentary credit for SMS messages and + inbound messages. +1. Create a number on twilio, and configure the voice request URL to be + ``https://your-app-id.appspot.com/call/receive`` and the SMS request URL to + be ``https://your-app-id.appspot.com/sms/receive``. +1. Configure your Twilio settings in the environment variables section in + [`src/main/webapp/WEB-INF/appengine-web.xml`](src/main/webapp/WEB-INF/appengine-web.xml). + +## Running locally + +You can run the application locally to test the callbacks and SMS sending. You +will need to set environment variables before starting your application: + + $ export TWILIO_ACCOUNT_SID=[your-twilio-accoun-sid] + $ export TWILIO_AUTH_TOKEN=[your-twilio-auth-token] + $ export TWILIO_NUMBER=[your-twilio-number] + $ mvn clean jetty:run diff --git a/appengine-java8/twilio/pom.xml b/appengine-java8/twilio/pom.xml new file mode 100644 index 00000000000..6ef9198a217 --- /dev/null +++ b/appengine-java8/twilio/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-twilio-j8 + + + appengine-java8-samples + com.google.cloud + 1.0.0 + .. + + + + + + com.twilio.sdk + twilio-java-sdk + 6.3.0 + + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/twilio/src/main/java/com/example/appengine/twilio/ReceiveCallServlet.java b/appengine-java8/twilio/src/main/java/com/example/appengine/twilio/ReceiveCallServlet.java new file mode 100644 index 00000000000..1e5aabda0cf --- /dev/null +++ b/appengine-java8/twilio/src/main/java/com/example/appengine/twilio/ReceiveCallServlet.java @@ -0,0 +1,49 @@ +/** + * Copyright 2015 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.example.appengine.twilio; + +import com.twilio.sdk.verbs.Say; +import com.twilio.sdk.verbs.TwiMLException; +import com.twilio.sdk.verbs.TwiMLResponse; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class ReceiveCallServlet extends HttpServlet { + + @Override + public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + TwiMLResponse twiml = new TwiMLResponse(); + Say say = new Say("Hello from Twilio!"); + try { + twiml.append(say); + } catch (TwiMLException e) { + throw new ServletException("Twilio error", e); + } + + resp.setContentType("application/xml"); + resp.getWriter().print(twiml.toXML()); + } +} +// [END example] diff --git a/appengine-java8/twilio/src/main/java/com/example/appengine/twilio/ReceiveSmsServlet.java b/appengine-java8/twilio/src/main/java/com/example/appengine/twilio/ReceiveSmsServlet.java new file mode 100644 index 00000000000..aa7bb6db308 --- /dev/null +++ b/appengine-java8/twilio/src/main/java/com/example/appengine/twilio/ReceiveSmsServlet.java @@ -0,0 +1,53 @@ +/** + * Copyright 2015 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.example.appengine.twilio; + +import com.twilio.sdk.verbs.Message; +import com.twilio.sdk.verbs.TwiMLException; +import com.twilio.sdk.verbs.TwiMLResponse; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class ReceiveSmsServlet extends HttpServlet { + + @Override + public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, + ServletException { + String fromNumber = request.getParameter("From"); + String body = request.getParameter("Body"); + String message = String.format("Hello, %s, you said %s", fromNumber, body); + + TwiMLResponse twiml = new TwiMLResponse(); + Message sms = new Message(message); + try { + twiml.append(sms); + } catch (TwiMLException e) { + throw new ServletException("Twilio error", e); + } + + response.setContentType("application/xml"); + response.getWriter().print(twiml.toXML()); + } +} +// [END example] diff --git a/appengine-java8/twilio/src/main/java/com/example/appengine/twilio/SendSmsServlet.java b/appengine-java8/twilio/src/main/java/com/example/appengine/twilio/SendSmsServlet.java new file mode 100644 index 00000000000..6cb0c30a46e --- /dev/null +++ b/appengine-java8/twilio/src/main/java/com/example/appengine/twilio/SendSmsServlet.java @@ -0,0 +1,67 @@ +/** + * Copyright 2015 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.example.appengine.twilio; + +import com.twilio.sdk.TwilioRestClient; +import com.twilio.sdk.TwilioRestException; +import com.twilio.sdk.resource.factory.MessageFactory; +import com.twilio.sdk.resource.instance.Account; +import com.twilio.sdk.resource.instance.Message; +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicNameValuePair; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +// [START example] +@SuppressWarnings("serial") +public class SendSmsServlet extends HttpServlet { + + @Override + public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + final String twilioAccountSid = System.getenv("TWILIO_ACCOUNT_SID"); + final String twilioAuthToken = System.getenv("TWILIO_AUTH_TOKEN"); + final String twilioNumber = System.getenv("TWILIO_NUMBER"); + final String toNumber = (String) req.getParameter("to"); + if (toNumber == null) { + resp.getWriter() + .print("Please provide the number to message in the \"to\" query string parameter."); + return; + } + TwilioRestClient client = new TwilioRestClient(twilioAccountSid, twilioAuthToken); + Account account = client.getAccount(); + MessageFactory messageFactory = account.getMessageFactory(); + List params = new ArrayList(); + params.add(new BasicNameValuePair("To", toNumber)); + params.add(new BasicNameValuePair("From", twilioNumber)); + params.add(new BasicNameValuePair("Body", "Hello from Twilio!")); + try { + Message sms = messageFactory.create(params); + resp.getWriter().print(sms.getBody()); + } catch (TwilioRestException e) { + throw new ServletException("Twilio error", e); + } + } +} +// [END example] diff --git a/appengine-java8/twilio/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/twilio/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..97cc58ce650 --- /dev/null +++ b/appengine-java8/twilio/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,24 @@ + + + + + java8 + true + + + + + + + diff --git a/appengine-java8/twilio/src/main/webapp/WEB-INF/web.xml b/appengine-java8/twilio/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..4791ebe69c4 --- /dev/null +++ b/appengine-java8/twilio/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,45 @@ + + + + + + + receivecall + com.example.appengine.twilio.ReceiveCallServlet + + + receivecall + /call/receive + + + receivesms + com.example.appengine.twilio.ReceiveSmsServlet + + + receivesms + /sms/receive + + + sendsms + com.example.appengine.twilio.SendSmsServlet + + + sendsms + /sms/send + + + diff --git a/appengine-java8/unittests/README.md b/appengine-java8/unittests/README.md new file mode 100644 index 00000000000..cd62f828b4f --- /dev/null +++ b/appengine-java8/unittests/README.md @@ -0,0 +1,9 @@ +# appengine-ndb-snippets + +## unittests + +This subdirectory contains code snippets for [Local Unit Testing for Java](https://cloud.google.com/appengine/docs/java/tools/localunittesting). + +App shows all the ways to unit test. + + mvn verify diff --git a/appengine-java8/unittests/pom.xml b/appengine-java8/unittests/pom.xml new file mode 100644 index 00000000000..a846a80f811 --- /dev/null +++ b/appengine-java8/unittests/pom.xml @@ -0,0 +1,94 @@ + + + + 4.0.0 + war + 1.0-SNAPSHOT + + + + appengine-java8-samples + com.google.cloud + 1.0.0 + .. + + + com.google.appengine.samples + unittests-appengine-local-testing-samples-j8 + + + 1.9.52 + UTF-8 + 1.22.0 + + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + jstl + jstl + 1.2 + + + + + junit + junit + 4.12 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.api-client + google-api-client-appengine + ${google-api-client.version} + test + + + + + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + + diff --git a/appengine-java8/unittests/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/unittests/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..a921b3b65d8 --- /dev/null +++ b/appengine-java8/unittests/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,9 @@ + + + java8 + true + + + + + diff --git a/appengine-java8/unittests/src/main/webapp/WEB-INF/logging.properties b/appengine-java8/unittests/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..a17206681f0 --- /dev/null +++ b/appengine-java8/unittests/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,13 @@ +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# + +# Set the default logging level for all loggers to WARNING +.level = WARNING diff --git a/appengine-java8/unittests/src/main/webapp/WEB-INF/queue.xml b/appengine-java8/unittests/src/main/webapp/WEB-INF/queue.xml new file mode 100644 index 00000000000..a0d8b77c060 --- /dev/null +++ b/appengine-java8/unittests/src/main/webapp/WEB-INF/queue.xml @@ -0,0 +1,10 @@ + + + default + 1/s + + + my-queue-name + 3/s + + diff --git a/appengine-java8/unittests/src/main/webapp/WEB-INF/web.xml b/appengine-java8/unittests/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..6a0e90311ab --- /dev/null +++ b/appengine-java8/unittests/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,7 @@ + + + diff --git a/appengine-java8/unittests/src/test/java/com/google/appengine/samples/AuthenticationTest.java b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/AuthenticationTest.java new file mode 100644 index 00000000000..c300702fb20 --- /dev/null +++ b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/AuthenticationTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 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.appengine.samples; + +// [START AuthenticationTest] + +import static org.junit.Assert.assertTrue; + +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class AuthenticationTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalUserServiceTestConfig()) + .setEnvIsAdmin(true).setEnvIsLoggedIn(true); + + @Before + public void setUp() { + helper.setUp(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void testIsAdmin() { + UserService userService = UserServiceFactory.getUserService(); + assertTrue(userService.isUserAdmin()); + } +} +// [END AuthenticationTest] diff --git a/appengine-java8/unittests/src/test/java/com/google/appengine/samples/DeferredTaskTest.java b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/DeferredTaskTest.java new file mode 100644 index 00000000000..3477305210e --- /dev/null +++ b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/DeferredTaskTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015 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.appengine.samples; + +// [START DeferredTaskTest] + +import static org.junit.Assert.assertTrue; + +import com.google.appengine.api.taskqueue.DeferredTask; +import com.google.appengine.api.taskqueue.QueueFactory; +import com.google.appengine.api.taskqueue.TaskOptions; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +public class DeferredTaskTest { + + // Unlike CountDownLatch, TaskCountDownlatch lets us reset. + private final LocalTaskQueueTestConfig.TaskCountDownLatch latch = + new LocalTaskQueueTestConfig.TaskCountDownLatch(1); + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalTaskQueueTestConfig() + .setDisableAutoTaskExecution(false) + .setCallbackClass(LocalTaskQueueTestConfig.DeferredTaskCallback.class) + .setTaskExecutionLatch(latch)); + + private static class MyTask implements DeferredTask { + private static boolean taskRan = false; + + @Override + public void run() { + taskRan = true; + } + } + + @Before + public void setUp() { + helper.setUp(); + } + + @After + public void tearDown() { + MyTask.taskRan = false; + latch.reset(); + helper.tearDown(); + } + + @Test + public void testTaskGetsRun() throws InterruptedException { + QueueFactory.getDefaultQueue().add( + TaskOptions.Builder.withPayload(new MyTask())); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(MyTask.taskRan); + } +} +// [END DeferredTaskTest] diff --git a/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalCustomPolicyHighRepDatastoreTest.java b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalCustomPolicyHighRepDatastoreTest.java new file mode 100644 index 00000000000..ef889c7579e --- /dev/null +++ b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalCustomPolicyHighRepDatastoreTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015 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.appengine.samples; + +// [START LocalCustomPolicyHighRepDatastoreTest] + +import static com.google.appengine.api.datastore.FetchOptions.Builder.withLimit; +import static org.junit.Assert.assertEquals; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.dev.HighRepJobPolicy; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class LocalCustomPolicyHighRepDatastoreTest { + private static final class CustomHighRepJobPolicy implements HighRepJobPolicy { + static int newJobCounter = 0; + static int existingJobCounter = 0; + + @Override + public boolean shouldApplyNewJob(Key entityGroup) { + // Every other new job fails to apply. + return newJobCounter++ % 2 == 0; + } + + @Override + public boolean shouldRollForwardExistingJob(Key entityGroup) { + // Every other existing job fails to apply. + return existingJobCounter++ % 2 == 0; + } + } + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig() + .setAlternateHighRepJobPolicyClass(CustomHighRepJobPolicy.class)); + + @Before + public void setUp() { + helper.setUp(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void testEventuallyConsistentGlobalQueryResult() { + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + ds.put(new Entity("yam")); // applies + ds.put(new Entity("yam")); // does not apply + // First global query only sees the first Entity. + assertEquals(1, ds.prepare(new Query("yam")).countEntities(withLimit(10))); + // Second global query sees both Entities because we "groom" (attempt to + // apply unapplied jobs) after every query. + assertEquals(2, ds.prepare(new Query("yam")).countEntities(withLimit(10))); + } +} +// [END LocalCustomPolicyHighRepDatastoreTest] diff --git a/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalDatastoreTest.java b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalDatastoreTest.java new file mode 100644 index 00000000000..2b451c8d65b --- /dev/null +++ b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalDatastoreTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 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.appengine.samples; + +// [START LocalDatastoreTest] + +import static com.google.appengine.api.datastore.FetchOptions.Builder.withLimit; +import static org.junit.Assert.assertEquals; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class LocalDatastoreTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig()); + + @Before + public void setUp() { + helper.setUp(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + // Run this test twice to prove we're not leaking any state across tests. + private void doTest() { + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + assertEquals(0, ds.prepare(new Query("yam")).countEntities(withLimit(10))); + ds.put(new Entity("yam")); + ds.put(new Entity("yam")); + assertEquals(2, ds.prepare(new Query("yam")).countEntities(withLimit(10))); + } + + @Test + public void testInsert1() { + doTest(); + } + + @Test + public void testInsert2() { + doTest(); + } +} + +// [END LocalDatastoreTest] diff --git a/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalHighRepDatastoreTest.java b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalHighRepDatastoreTest.java new file mode 100644 index 00000000000..8d696ad21d4 --- /dev/null +++ b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalHighRepDatastoreTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2015 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.appengine.samples; + +// [START LocalHighRepDatastoreTest] + +import static com.google.appengine.api.datastore.FetchOptions.Builder.withLimit; +import static org.junit.Assert.assertEquals; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class LocalHighRepDatastoreTest { + + // Maximum eventual consistency. + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig() + .setDefaultHighRepJobPolicyUnappliedJobPercentage(100)); + + @Before + public void setUp() { + helper.setUp(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void testEventuallyConsistentGlobalQueryResult() { + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + Key ancestor = KeyFactory.createKey("foo", 3); + ds.put(new Entity("yam", ancestor)); + ds.put(new Entity("yam", ancestor)); + // Global query doesn't see the data. + assertEquals(0, ds.prepare(new Query("yam")).countEntities(withLimit(10))); + // Ancestor query does see the data. + assertEquals(2, ds.prepare(new Query("yam", ancestor)).countEntities(withLimit(10))); + } +} +// [END LocalHighRepDatastoreTest] diff --git a/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalMemcacheTest.java b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalMemcacheTest.java new file mode 100644 index 00000000000..39186086cc2 --- /dev/null +++ b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalMemcacheTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015 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.appengine.samples; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +// [START imports] + +import com.google.appengine.api.memcache.MemcacheService; +import com.google.appengine.api.memcache.MemcacheServiceFactory; +import com.google.appengine.tools.development.testing.LocalMemcacheServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +// [END imports] + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +// [START NameAndHelper] +public class LocalMemcacheTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalMemcacheServiceTestConfig()); + + // [END NameAndHelper] + + @Before + public void setUp() { + helper.setUp(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + // Run this test twice to prove we're not leaking any state across tests. + // [START doTest] + private void doTest() { + MemcacheService ms = MemcacheServiceFactory.getMemcacheService(); + assertFalse(ms.contains("yar")); + ms.put("yar", "foo"); + assertTrue(ms.contains("yar")); + } + // [END doTest] + + @Test + public void testInsert1() { + doTest(); + } + + @Test + public void testInsert2() { + doTest(); + } +} diff --git a/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalUrlFetchTest.java b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalUrlFetchTest.java new file mode 100644 index 00000000000..b0e616f721e --- /dev/null +++ b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/LocalUrlFetchTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2016 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.appengine.samples; + +import static org.junit.Assert.assertEquals; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.appengine.tools.development.testing.LocalURLFetchServiceTestConfig; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; + +public class LocalUrlFetchTest { + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalURLFetchServiceTestConfig()); + + @Before + public void setUp() { + helper.setUp(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test + public void testMockUrlFetch() throws IOException { + // See http://g.co/dv/api-client-library/java/google-http-java-client/unit-testing + MockHttpTransport mockHttpTransport = new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + assertEquals(method, "GET"); + assertEquals(url, "http://foo.bar"); + + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(234); + return response; + } + }; + } + }; + + HttpRequestFactory requestFactory = mockHttpTransport.createRequestFactory(); + HttpResponse response = requestFactory.buildGetRequest(new GenericUrl("http://foo.bar")) + .execute(); + assertEquals(response.getStatusCode(), 234); + } +} diff --git a/appengine-java8/unittests/src/test/java/com/google/appengine/samples/MyFirstTest.java b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/MyFirstTest.java new file mode 100644 index 00000000000..f60eed48e1c --- /dev/null +++ b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/MyFirstTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 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.appengine.samples; + +// [START MyFirstTest] + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class MyFirstTest { + @Test + public void testAddition() { + assertEquals(4, 2 + 2); + } +} +// [END MyFirstTest] diff --git a/appengine-java8/unittests/src/test/java/com/google/appengine/samples/ShortTest.java b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/ShortTest.java new file mode 100644 index 00000000000..a024c08bcd3 --- /dev/null +++ b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/ShortTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 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.appengine.samples; + +// [START ShortTest] + +import static org.junit.Assert.assertEquals; + +import com.google.appengine.api.capabilities.Capability; +import com.google.appengine.api.capabilities.CapabilityStatus; +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.tools.development.testing.LocalCapabilitiesServiceTestConfig; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.apphosting.api.ApiProxy; + +import org.junit.After; +import org.junit.Test; + +public class ShortTest { + private LocalServiceTestHelper helper; + + @After + public void tearDown() { + helper.tearDown(); + } + + @Test(expected = ApiProxy.CapabilityDisabledException.class) + public void testDisabledDatastore() { + Capability testOne = new Capability("datastore_v3"); + CapabilityStatus testStatus = CapabilityStatus.DISABLED; + // Initialize the test configuration. + LocalCapabilitiesServiceTestConfig config = + new LocalCapabilitiesServiceTestConfig().setCapabilityStatus(testOne, testStatus); + helper = new LocalServiceTestHelper(config); + helper.setUp(); + FetchOptions fo = FetchOptions.Builder.withLimit(10); + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + assertEquals(0, ds.prepare(new Query("yam")).countEntities(fo)); + } +} + +// [END ShortTest] diff --git a/appengine-java8/unittests/src/test/java/com/google/appengine/samples/TaskQueueConfigTest.java b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/TaskQueueConfigTest.java new file mode 100644 index 00000000000..84e5d91b15f --- /dev/null +++ b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/TaskQueueConfigTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 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.appengine.samples; + +import static org.junit.Assert.assertEquals; + +import com.google.appengine.api.taskqueue.QueueFactory; +import com.google.appengine.api.taskqueue.TaskOptions; +import com.google.appengine.api.taskqueue.dev.LocalTaskQueue; +import com.google.appengine.api.taskqueue.dev.QueueStateInfo; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TaskQueueConfigTest { + // [START LocalServiceTestHelper] + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalTaskQueueTestConfig() + .setQueueXmlPath("src/main/webapp/WEB-INF/queue.xml")); + //[END LocalServiceTestHelper] + + @Before + public void setUp() { + helper.setUp(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + // Run this test twice to demonstrate we're not leaking state across tests. + // If we _are_ leaking state across tests we'll get an exception on the + // second test because there will already be a task with the given name. + private void doTest() throws InterruptedException { + // [START QueueFactory] + QueueFactory.getQueue("my-queue-name").add(TaskOptions.Builder.withTaskName("task29")); + // [END QueueFactory] + // Give the task time to execute if tasks are actually enabled (which they + // aren't, but that's part of the test). + Thread.sleep(1000); + LocalTaskQueue ltq = LocalTaskQueueTestConfig.getLocalTaskQueue(); + QueueStateInfo qsi = + ltq.getQueueStateInfo().get(QueueFactory.getQueue("my-queue-name").getQueueName()); + assertEquals(1, qsi.getTaskInfo().size()); + assertEquals("task29", qsi.getTaskInfo().get(0).getTaskName()); + } + + @Test + public void testTaskGetsScheduled1() throws InterruptedException { + doTest(); + } + + @Test + public void testTaskGetsScheduled2() throws InterruptedException { + doTest(); + } +} diff --git a/appengine-java8/unittests/src/test/java/com/google/appengine/samples/TaskQueueTest.java b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/TaskQueueTest.java new file mode 100644 index 00000000000..3616fb035e2 --- /dev/null +++ b/appengine-java8/unittests/src/test/java/com/google/appengine/samples/TaskQueueTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015 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.appengine.samples; + +// [START TaskQueueTest] + +import static org.junit.Assert.assertEquals; + +import com.google.appengine.api.taskqueue.QueueFactory; +import com.google.appengine.api.taskqueue.TaskOptions; +import com.google.appengine.api.taskqueue.dev.LocalTaskQueue; +import com.google.appengine.api.taskqueue.dev.QueueStateInfo; +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; +import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TaskQueueTest { + + private final LocalServiceTestHelper helper = + new LocalServiceTestHelper(new LocalTaskQueueTestConfig()); + + @Before + public void setUp() { + helper.setUp(); + } + + @After + public void tearDown() { + helper.tearDown(); + } + + // Run this test twice to demonstrate we're not leaking state across tests. + // If we _are_ leaking state across tests we'll get an exception on the + // second test because there will already be a task with the given name. + private void doTest() throws InterruptedException { + QueueFactory.getDefaultQueue().add(TaskOptions.Builder.withTaskName("task29")); + // Give the task time to execute if tasks are actually enabled (which they + // aren't, but that's part of the test). + Thread.sleep(1000); + LocalTaskQueue ltq = LocalTaskQueueTestConfig.getLocalTaskQueue(); + QueueStateInfo qsi = ltq.getQueueStateInfo().get(QueueFactory.getDefaultQueue().getQueueName()); + assertEquals(1, qsi.getTaskInfo().size()); + assertEquals("task29", qsi.getTaskInfo().get(0).getTaskName()); + } + + @Test + public void testTaskGetsScheduled1() throws InterruptedException { + doTest(); + } + + @Test + public void testTaskGetsScheduled2() throws InterruptedException { + doTest(); + } +} +// [END TaskQueueTest] diff --git a/appengine-java8/urlfetch/.gitignore b/appengine-java8/urlfetch/.gitignore new file mode 100644 index 00000000000..9b46a164a9f --- /dev/null +++ b/appengine-java8/urlfetch/.gitignore @@ -0,0 +1,11 @@ +# Eclipse files +.project +.classpath +.settings + +# Intellij +.idea/ +*.iml + +# Target folders +target/ diff --git a/appengine-java8/urlfetch/README.md b/appengine-java8/urlfetch/README.md new file mode 100644 index 00000000000..ac515ce7a55 --- /dev/null +++ b/appengine-java8/urlfetch/README.md @@ -0,0 +1,18 @@ +# Google App Engine Standard Environment URL Fetch Sample + +This sample demonstrates how to deploy an application on Google App Engine. + +See the [Google App Engine standard environment documentation][ae-docs] for more +detailed instructions. + +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Setup + + gcloud init + +## Running locally + mvn appengine:run + +## Deploying + mvn appengine:deploy diff --git a/appengine-java8/urlfetch/pom.xml b/appengine-java8/urlfetch/pom.xml new file mode 100644 index 00000000000..a6b40e024e3 --- /dev/null +++ b/appengine-java8/urlfetch/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-URLFetch-j8 + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + org.json + json + 20160810 + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/urlfetch/src/main/java/com/example/appengine/UrlFetchServlet.java b/appengine-java8/urlfetch/src/main/java/com/example/appengine/UrlFetchServlet.java new file mode 100644 index 00000000000..f0052655408 --- /dev/null +++ b/appengine-java8/urlfetch/src/main/java/com/example/appengine/UrlFetchServlet.java @@ -0,0 +1,103 @@ +/* + * Copyright 2016 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.example.appengine; + +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@SuppressWarnings("serial") +public class UrlFetchServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + +// [START example] + URL url = new URL("http://api.icndb.com/jokes/random"); + BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream())); + StringBuffer json = new StringBuffer(); + String line; + + while ((line = reader.readLine()) != null) { + json.append(line); + } + reader.close(); +// [END example] + JSONObject jo = new JSONObject(json.toString()); + + req.setAttribute("joke", jo.getJSONObject("value").getString("joke")); + req.getRequestDispatcher("/main.jsp").forward(req, resp); + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + + String id = req.getParameter("id"); + String text = req.getParameter("text"); + + if (id == null || text == null || id == "" || text == "") { + req.setAttribute("error", "invalid input"); + req.getRequestDispatcher("/main.jsp").forward(req, resp); + return; + } + + JSONObject jsonObj = new JSONObject() + .put("userId", 33) + .put("id", id) + .put("title", text) + .put("body", text); + + // [START complex] + URL url = new URL("http://jsonplaceholder.typicode.com/posts/" + id); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setDoOutput(true); + conn.setRequestMethod("PUT"); + + OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream()); + writer.write(URLEncoder.encode(jsonObj.toString(), "UTF-8")); + writer.close(); + + int respCode = conn.getResponseCode(); // New items get NOT_FOUND on PUT + if (respCode == HttpURLConnection.HTTP_OK || respCode == HttpURLConnection.HTTP_NOT_FOUND) { + req.setAttribute("error", ""); + StringBuffer response = new StringBuffer(); + String line; + + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + while ((line = reader.readLine()) != null) { + response.append(line); + } + reader.close(); + req.setAttribute("response", response.toString()); + } else { + req.setAttribute("error", conn.getResponseCode() + " " + conn.getResponseMessage()); + } + // [END complex] + req.getRequestDispatcher("/main.jsp").forward(req, resp); + } + +} diff --git a/appengine-java8/urlfetch/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/urlfetch/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..29bc35cee0b --- /dev/null +++ b/appengine-java8/urlfetch/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,5 @@ + + + java8 + true + diff --git a/appengine-java8/urlfetch/src/main/webapp/WEB-INF/web.xml b/appengine-java8/urlfetch/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..ddc5cfb4c46 --- /dev/null +++ b/appengine-java8/urlfetch/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,14 @@ + + + + hello + com.example.appengine.UrlFetchServlet + + + hello + / + + diff --git a/appengine-java8/urlfetch/src/main/webapp/main.jsp b/appengine-java8/urlfetch/src/main/webapp/main.jsp new file mode 100644 index 00000000000..a4967094874 --- /dev/null +++ b/appengine-java8/urlfetch/src/main/webapp/main.jsp @@ -0,0 +1,48 @@ +<%-- +Copyright 2016 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. +--%> +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> + + + URL Fetch sample + + + + +

URL Fetch Sample

+ +

Joke: ${joke}

+
+

+ +

${error}

+ +

+ ${response} +

+

+

+
+
+ +
+

+ + + + + + \ No newline at end of file diff --git a/appengine-java8/users/README.md b/appengine-java8/users/README.md new file mode 100644 index 00000000000..e58938a662a --- /dev/null +++ b/appengine-java8/users/README.md @@ -0,0 +1,20 @@ +# Users Authentication sample for Google App Engine + +This sample demonstrates how to use the [Users API][appid] on [Google App +Engine][ae-docs]. + +[appid]: https://cloud.google.com/appengine/docs/java/users/ +[ae-docs]: https://cloud.google.com/appengine/docs/java/ + +## Running locally +This example uses the +[Maven gcloud plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven). +To run this sample locally: + + $ mvn appengine:run + +## Deploying +In the following command, replace YOUR-PROJECT-ID with your +[Google Cloud Project ID](https://developers.google.com/console/help/new/#projectnumber). + + $ mvn appengine:deploy diff --git a/appengine-java8/users/pom.xml b/appengine-java8/users/pom.xml new file mode 100644 index 00000000000..16127e9f0fc --- /dev/null +++ b/appengine-java8/users/pom.xml @@ -0,0 +1,113 @@ + + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-users-j8 + + + + com.google.cloud + appengine-java8-samples + 1.0.0 + .. + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + com.google.guava + guava + 20.0 + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + org.json + json + 20160810 + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.32 + test + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + + + + + diff --git a/appengine-java8/users/src/main/java/com/example/appengine/users/UsersServlet.java b/appengine-java8/users/src/main/java/com/example/appengine/users/UsersServlet.java new file mode 100644 index 00000000000..d654fc78384 --- /dev/null +++ b/appengine-java8/users/src/main/java/com/example/appengine/users/UsersServlet.java @@ -0,0 +1,51 @@ +/* Copyright 2016 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. + */ + +// [START users_API_example] +package com.example.appengine.users; + +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; + +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class UsersServlet extends HttpServlet { + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + UserService userService = UserServiceFactory.getUserService(); + + String thisUrl = req.getRequestURI(); + + resp.setContentType("text/html"); + if (req.getUserPrincipal() != null) { + resp.getWriter().println("

Hello, " + + req.getUserPrincipal().getName() + + "! You can sign out.

"); + } else { + resp.getWriter().println("

Please sign in.

"); + } + } +} +// [END users_API_example] + diff --git a/appengine-java8/users/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java8/users/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..29bc35cee0b --- /dev/null +++ b/appengine-java8/users/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,5 @@ + + + java8 + true + diff --git a/appengine-java8/users/src/main/webapp/WEB-INF/web.xml b/appengine-java8/users/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..4b2b77234dc --- /dev/null +++ b/appengine-java8/users/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,14 @@ + + + + users + com.example.appengine.users.UsersServlet + + + users + / + + diff --git a/appengine-java8/users/src/test/java/com/example/appengine/users/UsersServletTest.java b/appengine-java8/users/src/test/java/com/example/appengine/users/UsersServletTest.java new file mode 100644 index 00000000000..ffc0a74785a --- /dev/null +++ b/appengine-java8/users/src/test/java/com/example/appengine/users/UsersServletTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015 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.example.appengine.users; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.appengine.tools.development.testing.LocalServiceTestHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.management.remote.JMXPrincipal; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Unit tests for {@link UsersServlet}. + */ +@RunWith(JUnit4.class) +public class UsersServletTest { + private static final String FAKE_URL = "fakey.fake.fak"; + private static final String FAKE_NAME = "Fake"; + // Set up a helper so that the ApiProxy returns a valid environment for local testing. + private final LocalServiceTestHelper helper = new LocalServiceTestHelper(); + + @Mock private HttpServletRequest mockRequestNotLoggedIn; + @Mock private HttpServletRequest mockRequestLoggedIn; + @Mock private HttpServletResponse mockResponse; + private StringWriter responseWriter; + private UsersServlet servletUnderTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + helper.setUp(); + + // Set up some fake HTTP requests + // If the user isn't logged in, use this request + when(mockRequestNotLoggedIn.getRequestURI()).thenReturn(FAKE_URL); + when(mockRequestNotLoggedIn.getUserPrincipal()).thenReturn(null); + + // If the user is logged in, use this request + when(mockRequestLoggedIn.getRequestURI()).thenReturn(FAKE_URL); + // Most of the classes that implement Principal have been + // deprecated. JMXPrincipal seems like a safe choice. + when(mockRequestLoggedIn.getUserPrincipal()).thenReturn(new JMXPrincipal(FAKE_NAME)); + + // Set up a fake HTTP response. + responseWriter = new StringWriter(); + when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter)); + + servletUnderTest = new UsersServlet(); + } + + @After public void tearDown() { + helper.tearDown(); + } + + @Test + public void doGet_userNotLoggedIn_writesResponse() throws Exception { + servletUnderTest.doGet(mockRequestNotLoggedIn, mockResponse); + + // If a user isn't logged in, we expect a prompt + // to login to be returned. + assertThat(responseWriter.toString()) + .named("UsersServlet response") + .contains("

Please .

"); + } + + @Test + public void doGet_userLoggedIn_writesResponse() throws Exception { + servletUnderTest.doGet(mockRequestLoggedIn, mockResponse); + + // If a user is logged in, we expect a prompt + // to logout to be returned. + assertThat(responseWriter.toString()) + .named("UsersServlet response") + .contains("

Hello, " + FAKE_NAME + "!"); + assertThat(responseWriter.toString()) + .named("UsersServlet response") + .contains("sign out"); + } +} diff --git a/appengine/.gitignore b/appengine/.gitignore deleted file mode 100644 index c94f025e98b..00000000000 --- a/appengine/.gitignore +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2015 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. - -# Google App Engine generated folder -appengine-generated/ - -# Java -*.class - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.ear - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - -# maven -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties - -service-account.json - -#eclipse -.classpath -.settings -.project diff --git a/appengine/appidentity/README.md b/appengine/appidentity/README.md index 5f5c13bd111..59a46a909a1 100644 --- a/appengine/appidentity/README.md +++ b/appengine/appidentity/README.md @@ -8,26 +8,11 @@ Engine][ae-docs]. ## Running locally This example uses the -[Maven gcloud plugin](https://cloud.google.com/appengine/docs/java/managed-vms/maven). +[Maven Cloud SDK plugin](https://cloud.google.com/appengine/docs/java/tools/using-maven). To run this sample locally: - $ mvn gcloud:run + $ mvn appengine:run ## Deploying -In the following command, replace YOUR-PROJECT-ID with your -[Google Cloud Project ID](https://developers.google.com/console/help/new/#projectnumber). - $ mvn gcloud:deploy -Dgcloud.gcloud_project=YOUR-PROJECT-ID - -## Setup -To save your project settings so that you don't need to enter the -`-Dgcloud.gcloud_project=YOUR-CLOUD-PROJECT-ID` parameters, you can: - -1. Update the tag in src/main/webapp/WEB-INF/appengine-web.xml - with your project name. - -You will now be able to run - - $ mvn gcloud:deploy - -without the need for any additional parameters. + $ mvn appengine:deploy diff --git a/appengine/appidentity/pom.xml b/appengine/appidentity/pom.xml index 8c950af5ac3..9a3c97bac20 100644 --- a/appengine/appidentity/pom.xml +++ b/appengine/appidentity/pom.xml @@ -28,7 +28,7 @@ - 2.0.9.133.v201611104 + 1.9.52 @@ -98,9 +98,13 @@ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes - com.google.appengine - gcloud-maven-plugin - ${gcloud-maven-plugin-version} + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + true + true + diff --git a/appengine/appidentity/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/appidentity/src/main/webapp/WEB-INF/appengine-web.xml index 21d1d476a5f..3136a5e9072 100644 --- a/appengine/appidentity/src/main/webapp/WEB-INF/appengine-web.xml +++ b/appengine/appidentity/src/main/webapp/WEB-INF/appengine-web.xml @@ -2,5 +2,4 @@ YOUR-PROJECT-ID true - true diff --git a/appengine/channel/README.md b/appengine/channel/README.md new file mode 100644 index 00000000000..0c478d5997b --- /dev/null +++ b/appengine/channel/README.md @@ -0,0 +1,5 @@ +AppEngine Channel demo +======== + +# WARNING - the Channel API has been deprecated. This sample is for historical purposes only. + diff --git a/pom.xml b/pom.xml index d04690edd50..756fb3ee783 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,7 @@ appengine + appengine-java8 flexible/analytics