From 7a7e40c0d6cc92c5200e1b86d49f8a9ce617ce81 Mon Sep 17 00:00:00 2001 From: Andrew Phillips Date: Tue, 1 May 2012 17:32:47 -0700 Subject: [PATCH] First cut of heroku-tweetstore --- demos/tweetstore/heroku-tweetstore/README.txt | 41 ++ demos/tweetstore/heroku-tweetstore/pom.xml | 129 ++++++ .../jclouds/demo/paas/PlatformServices.java | 64 +++ .../demo/paas/RunnableHttpRequest.java | 126 ++++++ .../demo/paas/config/HttpClientModule.java | 65 +++ .../config/PlatformServicesInitializer.java | 87 ++++ .../demo/paas/reference/PaasConstants.java | 28 ++ .../service/scheduler/HttpRequestJob.java | 69 +++ .../paas/service/scheduler/Scheduler.java | 41 ++ ...nlessXmlSchedulingDataProcessorPlugin.java | 401 ++++++++++++++++++ .../paas/service/taskqueue/TaskQueue.java | 107 +++++ .../tweetstore/config/GuiceServletConfig.java | 153 +++++++ .../config/util/CredentialsCollector.java | 153 +++++++ .../config/util/PropertiesLoader.java | 59 +++ .../controller/AddTweetsController.java | 96 +++++ .../controller/EnqueueStoresController.java | 101 +++++ .../controller/StoreTweetsController.java | 130 ++++++ .../tweetstore/domain/StoredTweetStatus.java | 149 +++++++ .../functions/KeyToStoredTweetStatus.java | 71 ++++ .../ServiceToStoredTweetStatuses.java | 73 ++++ .../reference/TweetStoreConstants.java | 34 ++ .../reference/TwitterConstants.java | 31 ++ .../src/main/platform/.gitignore | 1 + .../src/main/resources/jobs.xml | 29 ++ .../src/main/resources/quartz.properties | 28 ++ .../src/main/webapp/WEB-INF/web.xml | 60 +++ .../src/main/webapp/index.jsp | 31 ++ .../src/main/webapp/tweets.jsp | 109 +++++ .../config/util/CredentialsCollectorTest.java | 82 ++++ .../controller/AddTweetsControllerTest.java | 78 ++++ .../EnqueueStoresControllerTest.java | 85 ++++ .../controller/StoreTweetsControllerTest.java | 120 ++++++ .../functions/KeyToStoredTweetStatusTest.java | 69 +++ .../ServiceToStoredTweetStatusesTest.java | 75 ++++ .../tweetstore/integration/JettyServer.java | 68 +++ .../demo/tweetstore/integration/Runner2.java | 72 ++++ .../integration/TweetStoreLiveTest.java | 237 +++++++++++ .../integration/util/ObjectFields.java | 56 +++ .../src/test/resources/log4j.xml | 95 +++++ demos/tweetstore/pom.xml | 1 + 40 files changed, 3504 insertions(+) create mode 100644 demos/tweetstore/heroku-tweetstore/README.txt create mode 100644 demos/tweetstore/heroku-tweetstore/pom.xml create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/PlatformServices.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/RunnableHttpRequest.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/config/HttpClientModule.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/config/PlatformServicesInitializer.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/reference/PaasConstants.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/HttpRequestJob.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/Scheduler.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/quartz/plugins/TransactionlessXmlSchedulingDataProcessorPlugin.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/taskqueue/TaskQueue.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/GuiceServletConfig.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollector.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/util/PropertiesLoader.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/AddTweetsController.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresController.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/StoreTweetsController.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/domain/StoredTweetStatus.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatus.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatuses.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/reference/TweetStoreConstants.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/reference/TwitterConstants.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/platform/.gitignore create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/resources/jobs.xml create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/resources/quartz.properties create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/webapp/WEB-INF/web.xml create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/webapp/index.jsp create mode 100644 demos/tweetstore/heroku-tweetstore/src/main/webapp/tweets.jsp create mode 100644 demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollectorTest.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/AddTweetsControllerTest.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresControllerTest.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/StoreTweetsControllerTest.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatusTest.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatusesTest.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/JettyServer.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/Runner2.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/TweetStoreLiveTest.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/util/ObjectFields.java create mode 100644 demos/tweetstore/heroku-tweetstore/src/test/resources/log4j.xml diff --git a/demos/tweetstore/heroku-tweetstore/README.txt b/demos/tweetstore/heroku-tweetstore/README.txt new file mode 100644 index 0000000000..c6bf77b0dd --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/README.txt @@ -0,0 +1,41 @@ +==== + Licensed to jclouds, Inc. (jclouds) under one or more + contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. jclouds licenses this file + to you 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 guide to generating Twitter consumer keys and access tokens is at http://tinyurl.com/2fhebgb + +Please modify your maven settings.xml like below before attempting to run 'mvn -Plive install' + + + keys + + true + + + YOUR_ACCESS_KEY_ID + YOUR_SECRET_KEY + YOUR_USER + YOUR_HEX_KEY + YOUR_ACCOUNT + YOUR_BASE64_ENCODED_KEY + YOUR_TWITTER_CONSUMER_KEY + YOUR_TWITTER_CONSUMER_SECRET + YOUR_TWITTER_ACCESSTOKEN + YOUR_TWITTER_ACCESSTOKEN_SECRET + + \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/pom.xml b/demos/tweetstore/heroku-tweetstore/pom.xml new file mode 100644 index 0000000000..118ef206b4 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/pom.xml @@ -0,0 +1,129 @@ + + + + 4.0.0 + + org.jclouds + jclouds-demos-tweetstore-project + 1.5.0-SNAPSHOT + + jclouds-demo-heroku-tweetstore + war + jclouds TweetStore for Heroku + jclouds TweetStore for Heroku's Cedar using Guice for Dependency Injection + + + localhost + 8088 + jclouds-heroku-tweetstore + + + + + com.google.inject.extensions + guice-servlet + 3.0 + + + org.quartz-scheduler + quartz + 2.1.3 + + + org.slf4j + slf4j-api + + + + + + javax.servlet + servlet-api + 2.5 + + + + + org.mortbay.jetty + jetty-runner + 7.5.4.v20111024 + test + + + + + + live + + + + maven-surefire-plugin + + + integration + integration-test + + test + + + + ${test.twitter.runatcloud-tweetstore.consumer.identity} + ${test.twitter.runatcloud-tweetstore.consumer.credential} + ${test.twitter.runatcloud-tweetstore.access.identity} + ${test.twitter.runatcloud-tweetstore.access.credential} + ${test.azureblob.identity} + ${test.azureblob.credential} + ${test.cloudfiles-us.identity} + ${test.cloudfiles-us.credential} + ${test.aws-s3.identity} + ${test.aws-s3.credential} + ${test.cloudonestorage.identity} + ${test.cloudonestorage.credential} + ${test.ninefold-storage.identity} + ${test.ninefold-storage.credential} + ${test.jetty.address} + ${test.jetty.port} + ${project.build.directory}/jetty + ${jclouds.tweetstore.blobstores} + test.${jclouds.tweetstore.container} + ${project.build.directory}/${project.artifactId} + + + ${test.jetty.port} + + + + + + + + + + + deploy + + + heroku-tweetstore + + + + diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/PlatformServices.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/PlatformServices.java new file mode 100644 index 0000000000..0997005157 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/PlatformServices.java @@ -0,0 +1,64 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.paas; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.jclouds.demo.paas.config.PlatformServicesInitializer.PLATFORM_SERVICES_ATTRIBUTE_NAME; + +import java.util.Map; + +import javax.servlet.ServletContext; + +import org.jclouds.demo.paas.service.scheduler.Scheduler; +import org.jclouds.demo.paas.service.taskqueue.TaskQueue; +import org.jclouds.javax.annotation.Nullable; + +import com.google.common.collect.ImmutableMap; + +/** + * @author Andrew Phillips + */ +public class PlatformServices { + protected final String baseUrl; + protected final Scheduler scheduler; + private ImmutableMap taskQueues; + + public PlatformServices(String baseUrl, Scheduler scheduler, Map taskQueues) { + this.baseUrl = baseUrl; + this.scheduler = scheduler; + this.taskQueues = ImmutableMap.copyOf(taskQueues); + } + + public String getBaseUrl() { + return baseUrl; + } + + public Scheduler getScheduler() { + return scheduler; + } + + public @Nullable TaskQueue getTaskQueue(String name) { + return taskQueues.get(name); + } + + public static PlatformServices get(ServletContext context) { + return (PlatformServices) checkNotNull(context.getAttribute( + PLATFORM_SERVICES_ATTRIBUTE_NAME), PLATFORM_SERVICES_ATTRIBUTE_NAME); + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/RunnableHttpRequest.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/RunnableHttpRequest.java new file mode 100644 index 0000000000..ad72a1a58d --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/RunnableHttpRequest.java @@ -0,0 +1,126 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.paas; + +import static java.lang.String.format; + +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpCommandExecutorService; +import org.jclouds.http.HttpRequest; + +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; + +public class RunnableHttpRequest implements Runnable { + public static final String PLATFORM_REQUEST_ORIGINATOR_HEADER = "X-Platform-Originator"; + + public static Factory factory(HttpCommandExecutorService httpClient) { + return factory(httpClient, format("%s@%d", Factory.class.getName(), System.currentTimeMillis())); + } + + public static Factory factory(HttpCommandExecutorService httpClient, String originator) { + return new Factory(httpClient, originator); + } + + public static class Factory { + protected final HttpCommandExecutorService httpClient; + protected final String originator; + + private Factory(HttpCommandExecutorService httpClient, String originator) { + this.httpClient = httpClient; + this.originator = originator; + } + + public RunnableHttpRequest create(HttpRequest request) { + HttpRequest requestWithSubmitter = request.toBuilder() + .headers(copyOfWithEntry(request.getHeaders(), + PLATFORM_REQUEST_ORIGINATOR_HEADER, originator)).build(); + return new RunnableHttpRequest(httpClient, requestWithSubmitter); + } + + private static Multimap copyOfWithEntry( + Multimap multimap, K k1, V v1) { + return ImmutableMultimap.builder().putAll(multimap).put(k1, v1).build(); + } + } + + private final HttpCommandExecutorService httpClient; + private final HttpRequest request; + + private RunnableHttpRequest(HttpCommandExecutorService httpClient, HttpRequest request) { + this.httpClient = httpClient; + this.request = request; + } + + @Override + public void run() { + httpClient.submit(new ImmutableHttpCommand(request)); + } + + private class ImmutableHttpCommand implements HttpCommand { + private final HttpRequest request; + + public ImmutableHttpCommand(HttpRequest request) { + this.request = request; + } + + @Override + public void setException(Exception exception) { + } + + @Override + public void setCurrentRequest(HttpRequest request) { + } + + @Override + public boolean isReplayable() { + return false; + } + + @Override + public int incrementRedirectCount() { + return 0; + } + + @Override + public int incrementFailureCount() { + return 0; + } + + @Override + public int getRedirectCount() { + return 0; + } + + @Override + public int getFailureCount() { + return 0; + } + + @Override + public Exception getException() { + return null; + } + + @Override + public HttpRequest getCurrentRequest() { + return request; + } + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/config/HttpClientModule.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/config/HttpClientModule.java new file mode 100644 index 0000000000..5aa077b6cd --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/config/HttpClientModule.java @@ -0,0 +1,65 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.paas.config; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.inject.name.Names.bindProperties; +import static org.jclouds.Constants.*; + +import java.util.Properties; + +import javax.servlet.ServletContext; +import javax.ws.rs.core.UriBuilder; + +import org.jclouds.demo.tweetstore.config.util.PropertiesLoader; + +import com.google.inject.AbstractModule; +import com.sun.jersey.api.uri.UriBuilderImpl; + +/** + * @author Andrew Phillips + */ +public class HttpClientModule extends AbstractModule { + private final ServletContext context; + + HttpClientModule(ServletContext context) { + this.context = context; + } + + @Override + protected void configure() { + // URL connection defaults + Properties toBind = defaultProperties(); + toBind.putAll(checkNotNull(new PropertiesLoader(context).get(), "properties")); + toBind.putAll(System.getProperties()); + bindProperties(binder(), toBind); + bind(UriBuilder.class).to(UriBuilderImpl.class); + } + + private static Properties defaultProperties() { + Properties props = new Properties(); + props.setProperty(PROPERTY_MAX_CONNECTIONS_PER_CONTEXT, 20 + ""); + props.setProperty(PROPERTY_MAX_CONNECTIONS_PER_HOST, 0 + ""); + props.setProperty(PROPERTY_SO_TIMEOUT, 60000 + ""); + props.setProperty(PROPERTY_CONNECTION_TIMEOUT, 60000 + ""); + props.setProperty(PROPERTY_USER_THREADS, 0 + ""); + props.setProperty(PROPERTY_IO_WORKER_THREADS, 20 + ""); + return props; + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/config/PlatformServicesInitializer.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/config/PlatformServicesInitializer.java new file mode 100644 index 0000000000..364a07f3ec --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/config/PlatformServicesInitializer.java @@ -0,0 +1,87 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.paas.config; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.SECONDS; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.jclouds.concurrent.config.ExecutorServiceModule; +import org.jclouds.demo.paas.PlatformServices; +import org.jclouds.demo.paas.service.scheduler.Scheduler; +import org.jclouds.demo.paas.service.taskqueue.TaskQueue; +import org.jclouds.http.HttpCommandExecutorService; +import org.jclouds.http.config.JavaUrlHttpCommandExecutorServiceModule; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import com.google.inject.Guice; + +/** + * @author Andrew Phillips + */ +public class PlatformServicesInitializer implements ServletContextListener { + public static final String PLATFORM_SERVICES_ATTRIBUTE_NAME = PlatformServices.class.getName(); + + protected static final String PORT_VARIABLE = "PORT"; + + @Override + public void contextInitialized(ServletContextEvent contextEvent) { + ServletContext context = contextEvent.getServletContext(); + context.setAttribute(PLATFORM_SERVICES_ATTRIBUTE_NAME, createServices(context)); + } + + protected static PlatformServices createServices(ServletContext context) { + HttpCommandExecutorService httpClient = createHttpClient(context); + return new PlatformServices(getBaseUrl(context), new Scheduler(httpClient), + createTaskQueues(httpClient)); + } + + protected static HttpCommandExecutorService createHttpClient( + final ServletContext context) { + return Guice.createInjector(new ExecutorServiceModule(), + new JavaUrlHttpCommandExecutorServiceModule(), + new HttpClientModule(context)) + .getInstance(HttpCommandExecutorService.class); + } + + protected static String getBaseUrl(ServletContext context) { + return format("http://localhost:%s%s", checkNotNull(System.getenv(PORT_VARIABLE), PORT_VARIABLE), + context.getContextPath()); + } + + // TODO: make the number and names of queues configurable + protected static ImmutableMap createTaskQueues(HttpCommandExecutorService httpClient) { + Builder taskQueues = ImmutableMap.builder(); + taskQueues.put("twitter", TaskQueue.builder(httpClient) + .name("twitter").period(SECONDS.toMillis(30)) + .build()); + return taskQueues.build(); + } + + @Override + public void contextDestroyed(ServletContextEvent servletContextEvent) { + ServletContext context = servletContextEvent.getServletContext(); + context.removeAttribute(PLATFORM_SERVICES_ATTRIBUTE_NAME); + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/reference/PaasConstants.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/reference/PaasConstants.java new file mode 100644 index 0000000000..8af7021bd2 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/reference/PaasConstants.java @@ -0,0 +1,28 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.paas.reference; + +/** + * Configuration properties and constants used in PaaS applications. + * + * @author Andrew Phillips + */ +public interface PaasConstants { + static final String PROPERTY_PLATFORM_BASE_URL = "jclouds.paas.baseurl"; +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/HttpRequestJob.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/HttpRequestJob.java new file mode 100644 index 0000000000..902f5fe356 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/HttpRequestJob.java @@ -0,0 +1,69 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.paas.service.scheduler; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.net.URI; + +import javax.servlet.ServletContext; + +import org.jclouds.demo.paas.PlatformServices; +import org.jclouds.demo.paas.RunnableHttpRequest; +import org.jclouds.http.HttpRequest; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerException; + +/** + * @author Andrew Phillips + */ +public class HttpRequestJob implements Job { + protected static final String URL_ATTRIBUTE_NAME = "url"; + + // keep in sync with "quartz:scheduler-context-servlet-context-key" param in web.xml + protected static final String SERVLET_CONTEXT_KEY = "servlet-context"; + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + PlatformServices platform = JobContexts.getPlatform(context); + RunnableHttpRequest request = platform.getScheduler().getHttpRequestFactory().create( + HttpRequest.builder() + .endpoint(JobContexts.getTargetUrl(platform.getBaseUrl(), context)) + .method("GET").build()); + request.run(); + } + + private static class JobContexts { + private static URI getTargetUrl(String baseUrl, JobExecutionContext context) { + return URI.create(baseUrl + (String) checkNotNull( + context.getMergedJobDataMap().get(URL_ATTRIBUTE_NAME), URL_ATTRIBUTE_NAME)); + } + + private static PlatformServices getPlatform(JobExecutionContext jobContext) throws JobExecutionException { + try { + return PlatformServices.get((ServletContext) checkNotNull( + jobContext.getScheduler().getContext().get(SERVLET_CONTEXT_KEY), SERVLET_CONTEXT_KEY)); + } catch (SchedulerException exception) { + throw new JobExecutionException("Unable to get platform services from the job execution context", exception); + } + } + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/Scheduler.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/Scheduler.java new file mode 100644 index 0000000000..dabdff877b --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/Scheduler.java @@ -0,0 +1,41 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.paas.service.scheduler; + +import org.jclouds.demo.paas.RunnableHttpRequest; +import org.jclouds.demo.paas.RunnableHttpRequest.Factory; +import org.jclouds.http.HttpCommandExecutorService; + +/** + * @author Andrew Phillips + */ +public class Scheduler { + protected static final String SCHEDULER_ORIGINATOR_NAME = "scheduler"; + + protected final Factory httpRequestFactory; + + public Scheduler(HttpCommandExecutorService httpClient) { + httpRequestFactory = + RunnableHttpRequest.factory(httpClient, SCHEDULER_ORIGINATOR_NAME); + } + + public Factory getHttpRequestFactory() { + return httpRequestFactory; + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/quartz/plugins/TransactionlessXmlSchedulingDataProcessorPlugin.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/quartz/plugins/TransactionlessXmlSchedulingDataProcessorPlugin.java new file mode 100644 index 0000000000..91659d9b16 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/scheduler/quartz/plugins/TransactionlessXmlSchedulingDataProcessorPlugin.java @@ -0,0 +1,401 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.paas.service.scheduler.quartz.plugins; + +import static org.quartz.SimpleScheduleBuilder.simpleSchedule; +import static org.quartz.TriggerBuilder.newTrigger; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +import org.jclouds.logging.Logger; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SimpleTrigger; +import org.quartz.TriggerKey; +import org.quartz.jobs.FileScanJob; +import org.quartz.jobs.FileScanListener; +import org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin; +import org.quartz.simpl.CascadingClassLoadHelper; +import org.quartz.spi.ClassLoadHelper; +import org.quartz.spi.SchedulerPlugin; +import org.quartz.xml.XMLSchedulingDataProcessor; + +/** + * A copy of {@link XMLSchedulingDataProcessorPlugin} that does not reference + * {@code javax.transaction.UserTransaction} as so does not require a dependency + * on JTA. + * + * @author Andrew Phillips + * @see XMLSchedulingDataProcessorPlugin + */ +public class TransactionlessXmlSchedulingDataProcessorPlugin implements + FileScanListener, SchedulerPlugin { + + /* + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Data members. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + private static final int MAX_JOB_TRIGGER_NAME_LEN = 80; + private static final String JOB_INITIALIZATION_PLUGIN_NAME = "JobSchedulingDataLoaderPlugin"; + private static final String FILE_NAME_DELIMITERS = ","; + + private String name; + private Scheduler scheduler; + private final Logger log = Logger.CONSOLE; + + private boolean failOnFileNotFound = true; + + private String fileNames = XMLSchedulingDataProcessor.QUARTZ_XML_DEFAULT_FILE_NAME; + + // Populated by initialization + private Map jobFiles = new LinkedHashMap(); + + private long scanInterval = 0; + + boolean started = false; + + protected ClassLoadHelper classLoadHelper = null; + + private Set jobTriggerNameSet = new HashSet(); + + /* + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Interface. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + + /** + * Comma separated list of file names (with paths) to the XML files that should be read. + */ + public String getFileNames() { + return fileNames; + } + + /** + * The file name (and path) to the XML file that should be read. + */ + public void setFileNames(String fileNames) { + this.fileNames = fileNames; + } + + /** + * The interval (in seconds) at which to scan for changes to the file. + * If the file has been changed, it is re-loaded and parsed. The default + * value for the interval is 0, which disables scanning. + * + * @return Returns the scanInterval. + */ + public long getScanInterval() { + return scanInterval / 1000; + } + + /** + * The interval (in seconds) at which to scan for changes to the file. + * If the file has been changed, it is re-loaded and parsed. The default + * value for the interval is 0, which disables scanning. + * + * @param scanInterval The scanInterval to set. + */ + public void setScanInterval(long scanInterval) { + this.scanInterval = scanInterval * 1000; + } + + /** + * Whether or not initialization of the plugin should fail (throw an + * exception) if the file cannot be found. Default is true. + */ + public boolean isFailOnFileNotFound() { + return failOnFileNotFound; + } + + /** + * Whether or not initialization of the plugin should fail (throw an + * exception) if the file cannot be found. Default is true. + */ + public void setFailOnFileNotFound(boolean failOnFileNotFound) { + this.failOnFileNotFound = failOnFileNotFound; + } + + /* + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * SchedulerPlugin Interface. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + + /** + *

+ * Called during creation of the Scheduler in order to give + * the SchedulerPlugin a chance to initialize. + *

+ * + * @throws org.quartz.SchedulerConfigException + * if there is an error initializing. + */ + @Override + public void initialize(String name, Scheduler scheduler) + throws SchedulerException { + this.name = name; + this.scheduler = scheduler; + + classLoadHelper = new CascadingClassLoadHelper(); + classLoadHelper.initialize(); + + log.info("Registering Quartz Job Initialization Plug-in."); + + // Create JobFile objects + StringTokenizer stok = new StringTokenizer(fileNames, FILE_NAME_DELIMITERS); + while (stok.hasMoreTokens()) { + final String fileName = stok.nextToken(); + final JobFile jobFile = new JobFile(fileName); + jobFiles.put(fileName, jobFile); + } + } + + @Override + public void start() { + try { + if (jobFiles.isEmpty() == false) { + + if (scanInterval > 0) { + scheduler.getContext().put(JOB_INITIALIZATION_PLUGIN_NAME + '_' + name, this); + } + + Iterator iterator = jobFiles.values().iterator(); + while (iterator.hasNext()) { + JobFile jobFile = iterator.next(); + + if (scanInterval > 0) { + String jobTriggerName = buildJobTriggerName(jobFile.getFileBasename()); + TriggerKey tKey = new TriggerKey(jobTriggerName, JOB_INITIALIZATION_PLUGIN_NAME); + + // remove pre-existing job/trigger, if any + scheduler.unscheduleJob(tKey); + + // TODO: convert to use builder + SimpleTrigger trig = newTrigger() + .withIdentity(jobTriggerName, JOB_INITIALIZATION_PLUGIN_NAME) + .startNow() + .endAt(null) + .withSchedule(simpleSchedule() + .repeatForever() + .withIntervalInMilliseconds(scanInterval)) + .build(); + + JobDetail job = JobBuilder.newJob(FileScanJob.class) + .withIdentity(jobTriggerName, JOB_INITIALIZATION_PLUGIN_NAME) + .build(); + job.getJobDataMap().put(FileScanJob.FILE_NAME, jobFile.getFileName()); + job.getJobDataMap().put(FileScanJob.FILE_SCAN_LISTENER_NAME, JOB_INITIALIZATION_PLUGIN_NAME + '_' + name); + + scheduler.scheduleJob(job, trig); + log.debug("Scheduled file scan job for data file: {}, at interval: {}", jobFile.getFileName(), scanInterval); + } + + processFile(jobFile); + } + } + } catch(SchedulerException se) { + log.error("Error starting background-task for watching jobs file.", se); + } finally { + started = true; + } + } + + /** + * Helper method for generating unique job/trigger name for the + * file scanning jobs (one per FileJob). The unique names are saved + * in jobTriggerNameSet. + */ + private String buildJobTriggerName( + String fileBasename) { + // Name w/o collisions will be prefix + _ + filename (with '.' of filename replaced with '_') + // For example: JobInitializationPlugin_jobInitializer_myjobs_xml + String jobTriggerName = JOB_INITIALIZATION_PLUGIN_NAME + '_' + name + '_' + fileBasename.replace('.', '_'); + + // If name is too long (DB column is 80 chars), then truncate to max length + if (jobTriggerName.length() > MAX_JOB_TRIGGER_NAME_LEN) { + jobTriggerName = jobTriggerName.substring(0, MAX_JOB_TRIGGER_NAME_LEN); + } + + // Make sure this name is unique in case the same file name under different + // directories is being checked, or had a naming collision due to length truncation. + // If there is a conflict, keep incrementing a _# suffix on the name (being sure + // not to get too long), until we find a unique name. + int currentIndex = 1; + while (jobTriggerNameSet.add(jobTriggerName) == false) { + // If not our first time through, then strip off old numeric suffix + if (currentIndex > 1) { + jobTriggerName = jobTriggerName.substring(0, jobTriggerName.lastIndexOf('_')); + } + + String numericSuffix = "_" + currentIndex++; + + // If the numeric suffix would make the name too long, then make room for it. + if (jobTriggerName.length() > (MAX_JOB_TRIGGER_NAME_LEN - numericSuffix.length())) { + jobTriggerName = jobTriggerName.substring(0, (MAX_JOB_TRIGGER_NAME_LEN - numericSuffix.length())); + } + + jobTriggerName += numericSuffix; + } + + return jobTriggerName; + } + + @Override + public void shutdown() { + // nothing to do + } + + private void processFile(JobFile jobFile) { + if (jobFile == null || !jobFile.getFileFound()) { + return; + } + + try { + XMLSchedulingDataProcessor processor = + new XMLSchedulingDataProcessor(this.classLoadHelper); + + processor.addJobGroupToNeverDelete(JOB_INITIALIZATION_PLUGIN_NAME); + processor.addTriggerGroupToNeverDelete(JOB_INITIALIZATION_PLUGIN_NAME); + + processor.processFileAndScheduleJobs( + jobFile.getFileName(), + jobFile.getFileName(), // systemId + scheduler); + } catch (Exception e) { + log.error("Error scheduling jobs: " + e.getMessage(), e); + } + } + + public void processFile(String filePath) { + processFile((JobFile)jobFiles.get(filePath)); + } + + /** + * @see org.quartz.jobs.FileScanListener#fileUpdated(java.lang.String) + */ + public void fileUpdated(String fileName) { + if (started) { + processFile(fileName); + } + } + + class JobFile { + private String fileName; + + // These are set by initialize() + private String filePath; + private String fileBasename; + private boolean fileFound; + + protected JobFile(String fileName) throws SchedulerException { + this.fileName = fileName; + initialize(); + } + + protected String getFileName() { + return fileName; + } + + protected boolean getFileFound() { + return fileFound; + } + + protected String getFilePath() { + return filePath; + } + + protected String getFileBasename() { + return fileBasename; + } + + private void initialize() throws SchedulerException { + InputStream f = null; + try { + String furl = null; + + File file = new File(getFileName()); // files in filesystem + if (!file.exists()) { + URL url = classLoadHelper.getResource(getFileName()); + if(url != null) { + try { + furl = URLDecoder.decode(url.getPath(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + furl = url.getPath(); + } + file = new File(furl); + try { + f = url.openStream(); + } catch (IOException ignor) { + // Swallow the exception + } + } + } else { + try { + f = new java.io.FileInputStream(file); + }catch (FileNotFoundException e) { + // ignore + } + } + + if (f == null) { + if (isFailOnFileNotFound()) { + throw new SchedulerException( + "File named '" + getFileName() + "' does not exist."); + } else { + log.warn("File named '" + getFileName() + "' does not exist."); + } + } else { + fileFound = true; + } + filePath = (furl != null) ? furl : file.getAbsolutePath(); + fileBasename = file.getName(); + } finally { + try { + if (f != null) { + f.close(); + } + } catch (IOException ioe) { + log.warn("Error closing jobs file " + getFileName(), ioe); + } + } + } + } +} \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/taskqueue/TaskQueue.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/taskqueue/TaskQueue.java new file mode 100644 index 0000000000..e317a305cf --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/paas/service/taskqueue/TaskQueue.java @@ -0,0 +1,107 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.paas.service.taskqueue; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; + +import org.jclouds.demo.paas.RunnableHttpRequest; +import org.jclouds.demo.paas.RunnableHttpRequest.Factory; +import org.jclouds.http.HttpCommandExecutorService; + +import com.google.inject.Provider; + +public class TaskQueue { + protected final Factory httpRequestFactory; + private final Timer timer; + private final ConcurrentLinkedQueue tasks = new ConcurrentLinkedQueue(); + + private TaskQueue(String name, long pollingIntervalMillis, Factory httpRequestFactory) { + this.httpRequestFactory = httpRequestFactory; + timer = new Timer(name); + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + Runnable task = tasks.poll(); + if (task != null) { + task.run(); + } + } + }, 0, pollingIntervalMillis); + } + + public void add(final Runnable task) { + tasks.add(task); + } + + public Factory getHttpRequestFactory() { + return httpRequestFactory; + } + + public void destroy() { + timer.cancel(); + tasks.clear(); + } + + public static Builder builder(HttpCommandExecutorService httpClient) { + return new Builder(httpClient); + } + + public static class Builder implements Provider { + protected final HttpCommandExecutorService httpClient; + protected String name = "default"; + protected long pollingIntervalMillis = TimeUnit.SECONDS.toMillis(1); + + private Builder(HttpCommandExecutorService httpClient) { + this.httpClient = checkNotNull(httpClient, "httpClient"); + } + + public Builder name(String name) { + this.name = checkNotNull(name, "name"); + return this; + } + + public Builder period(TimeUnit period) { + this.pollingIntervalMillis = checkNotNull(period, "period").toMillis(1); + return this; + } + + public Builder period(long pollingIntervalMillis) { + checkArgument(pollingIntervalMillis > 0, "pollingIntervalMillis"); + this.pollingIntervalMillis = pollingIntervalMillis; + return this; + } + + public TaskQueue build() { + return new TaskQueue(name, pollingIntervalMillis, + RunnableHttpRequest.factory(httpClient, format("taskqueue-%s", name))); + } + + @Override + public TaskQueue get() { + return build(); + } + } +} \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/GuiceServletConfig.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/GuiceServletConfig.java new file mode 100644 index 0000000000..24d0c94c06 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/GuiceServletConfig.java @@ -0,0 +1,153 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.config; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.in; +import static com.google.common.collect.ImmutableSet.copyOf; +import static com.google.common.collect.Sets.filter; +import static org.jclouds.demo.paas.reference.PaasConstants.PROPERTY_PLATFORM_BASE_URL; +import static org.jclouds.demo.tweetstore.reference.TweetStoreConstants.PROPERTY_TWEETSTORE_BLOBSTORES; +import static org.jclouds.demo.tweetstore.reference.TweetStoreConstants.PROPERTY_TWEETSTORE_CONTAINER; +import static org.jclouds.demo.tweetstore.reference.TwitterConstants.PROPERTY_TWITTER_ACCESSTOKEN; +import static org.jclouds.demo.tweetstore.reference.TwitterConstants.PROPERTY_TWITTER_ACCESSTOKEN_SECRET; +import static org.jclouds.demo.tweetstore.reference.TwitterConstants.PROPERTY_TWITTER_CONSUMER_KEY; +import static org.jclouds.demo.tweetstore.reference.TwitterConstants.PROPERTY_TWITTER_CONSUMER_SECRET; + +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; + +import org.jclouds.ContextBuilder; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.demo.paas.PlatformServices; +import org.jclouds.demo.paas.service.taskqueue.TaskQueue; +import org.jclouds.demo.tweetstore.config.util.CredentialsCollector; +import org.jclouds.demo.tweetstore.config.util.PropertiesLoader; +import org.jclouds.demo.tweetstore.controller.AddTweetsController; +import org.jclouds.demo.tweetstore.controller.EnqueueStoresController; +import org.jclouds.demo.tweetstore.controller.StoreTweetsController; + +import twitter4j.Twitter; +import twitter4j.TwitterFactory; +import twitter4j.conf.Configuration; +import twitter4j.conf.ConfigurationBuilder; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.google.inject.name.Names; +import com.google.inject.servlet.GuiceServletContextListener; +import com.google.inject.servlet.ServletModule; + +/** + * Setup Logging and create Injector for use in testing S3. + * + * @author Adrian Cole + */ +public class GuiceServletConfig extends GuiceServletContextListener { + private Map providerTypeToBlobStoreMap; + private Twitter twitterClient; + private String container; + private TaskQueue queue; + private String baseUrl; + + @Override + public void contextInitialized(ServletContextEvent servletContextEvent) { + ServletContext servletContext = servletContextEvent.getServletContext(); + + Properties props = new PropertiesLoader(servletContext).get(); + Set modules = ImmutableSet.of(); + // shared across all blobstores and used to retrieve tweets + try { + Configuration twitterConf = new ConfigurationBuilder() + .setOAuthConsumerKey(props.getProperty(PROPERTY_TWITTER_CONSUMER_KEY)) + .setOAuthConsumerSecret(props.getProperty(PROPERTY_TWITTER_CONSUMER_SECRET)) + .setOAuthAccessToken(props.getProperty(PROPERTY_TWITTER_ACCESSTOKEN)) + .setOAuthAccessTokenSecret(props.getProperty(PROPERTY_TWITTER_ACCESSTOKEN_SECRET)) + .build(); + twitterClient = new TwitterFactory(twitterConf).getInstance(); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("properties for twitter not configured properly in " + props.toString(), e); + } + // common namespace for storing tweets + container = checkNotNull(props.getProperty(PROPERTY_TWEETSTORE_CONTAINER), PROPERTY_TWEETSTORE_CONTAINER); + + // instantiate and store references to all blobstores by provider name + providerTypeToBlobStoreMap = Maps.newHashMap(); + for (String hint : getBlobstoreContexts(props)) { + providerTypeToBlobStoreMap.put(hint, ContextBuilder.newBuilder(hint) + .modules(modules).overrides(props).build(BlobStoreContext.class)); + } + + // get a queue for submitting store tweet requests and the application's base URL + PlatformServices platform = PlatformServices.get(servletContext); + queue = platform.getTaskQueue("twitter"); + baseUrl = platform.getBaseUrl(); + + super.contextInitialized(servletContextEvent); + } + + private static Iterable getBlobstoreContexts(Properties props) { + Set contexts = new CredentialsCollector().apply(props).keySet(); + String explicitContexts = props.getProperty(PROPERTY_TWEETSTORE_BLOBSTORES); + if (explicitContexts != null) { + contexts = filter(contexts, in(copyOf(Splitter.on(',').split(explicitContexts)))); + } + checkState(!contexts.isEmpty(), "no credentials available for any requested context"); + return contexts; + } + + @Override + protected Injector getInjector() { + return Guice.createInjector(new ServletModule() { + @Override + protected void configureServlets() { + bind(new TypeLiteral>() {}) + .toInstance(providerTypeToBlobStoreMap); + bind(Twitter.class).toInstance(twitterClient); + bind(TaskQueue.class).toInstance(queue); + bindConstant().annotatedWith(Names.named(PROPERTY_PLATFORM_BASE_URL)) + .to(baseUrl); + bindConstant().annotatedWith(Names.named(PROPERTY_TWEETSTORE_CONTAINER)) + .to(container); + serve("/store/*").with(StoreTweetsController.class); + serve("/tweets/*").with(AddTweetsController.class); + serve("/stores/*").with(EnqueueStoresController.class); + } + }); + } + + @Override + public void contextDestroyed(ServletContextEvent servletContextEvent) { + for (BlobStoreContext context : providerTypeToBlobStoreMap.values()) { + context.close(); + } + queue.destroy(); + super.contextDestroyed(servletContextEvent); + } +} \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollector.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollector.java new file mode 100644 index 0000000000..ce3943376e --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollector.java @@ -0,0 +1,153 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.config.util; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.notNull; +import static com.google.common.collect.Collections2.filter; +import static com.google.common.collect.Collections2.transform; +import static com.google.common.collect.ImmutableSet.copyOf; +import static com.google.common.collect.Maps.filterValues; +import static org.jclouds.util.Maps2.fromKeys; + +import java.util.Collection; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jclouds.demo.tweetstore.config.util.CredentialsCollector.Credential; + +import com.google.common.annotations.GwtIncompatible; +import com.google.common.base.Function; +import com.google.common.base.Predicate; + +/** + * Reads provider credentials from a {@link Properties} bag. + * + * @author Andrew Phillips + * + */ +public class CredentialsCollector implements Function> { + private static final String IDENTITY_PROPERTY_SUFFIX = ".identity"; + private static final String CREDENTIAL_PROPERTY_SUFFIX = ".credential"; + + // using the identity for provider name extraction + private static final Pattern IDENTITY_PROPERTY_PATTERN = + Pattern.compile("([a-zA-Z0-9-]+)" + Pattern.quote(IDENTITY_PROPERTY_SUFFIX)); + + @Override + public Map apply(final Properties properties) { + Collection providerNames = transform( + filter(properties.stringPropertyNames(), MatchesPattern.matches(IDENTITY_PROPERTY_PATTERN)), + new Function() { + @Override + public String apply(String input) { + Matcher matcher = IDENTITY_PROPERTY_PATTERN.matcher(input); + // as a side-effect, sets the matching group! + checkState(matcher.matches(), "'%s' should match '%s'", input, IDENTITY_PROPERTY_PATTERN); + return matcher.group(1); + } + }); + /* + * Providers without a credential property result in null values, which are + * removed from the returned map. + */ + return filterValues(fromKeys(copyOf(providerNames), new Function() { + @Override + public Credential apply(String providerName) { + String identity = properties.getProperty(providerName + IDENTITY_PROPERTY_SUFFIX); + String credential = properties.getProperty(providerName + CREDENTIAL_PROPERTY_SUFFIX); + return (((identity != null) && (credential != null)) + ? new Credential(identity, credential) + : null); + } + }), notNull()); + } + + public static class Credential { + private final String identity; + private final String credential; + + public Credential(String identity, String credential) { + this.identity = checkNotNull(identity, "identity"); + this.credential = checkNotNull(credential, "credential"); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((credential == null) ? 0 : credential.hashCode()); + result = prime * result + + ((identity == null) ? 0 : identity.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Credential other = (Credential) obj; + if (credential == null) { + if (other.credential != null) + return false; + } else if (!credential.equals(other.credential)) + return false; + if (identity == null) { + if (other.identity != null) + return false; + } else if (!identity.equals(other.identity)) + return false; + return true; + } + + public String getIdentity() { + return identity; + } + + public String getCredential() { + return credential; + } + } + + @GwtIncompatible(value = "java.util.regex.Pattern") + private static class MatchesPattern implements Predicate { + private final Pattern pattern; + + private MatchesPattern(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public boolean apply(String input) { + return pattern.matcher(input).matches(); + } + + private static MatchesPattern matches(Pattern pattern) { + return new MatchesPattern(pattern); + } + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/util/PropertiesLoader.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/util/PropertiesLoader.java new file mode 100644 index 0000000000..dafa5c311b --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/config/util/PropertiesLoader.java @@ -0,0 +1,59 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.config.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import javax.servlet.ServletContext; + +import com.google.common.io.Closeables; +import com.google.inject.Provider; + +/** + * @author Andrew Phillips + */ +public class PropertiesLoader implements Provider{ + private static final String PROPERTIES_FILE = "/WEB-INF/jclouds.properties"; + + private final Properties properties; + + public PropertiesLoader(ServletContext context) { + properties = loadJcloudsProperties(context); + } + + private static Properties loadJcloudsProperties(ServletContext context) { + InputStream input = context.getResourceAsStream(PROPERTIES_FILE); + Properties props = new Properties(); + try { + props.load(input); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + Closeables.closeQuietly(input); + } + return props; + } + + @Override + public Properties get() { + return properties; + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/AddTweetsController.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/AddTweetsController.java new file mode 100644 index 0000000000..007fbafdef --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/AddTweetsController.java @@ -0,0 +1,96 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.controller; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.demo.tweetstore.domain.StoredTweetStatus; +import org.jclouds.demo.tweetstore.functions.ServiceToStoredTweetStatuses; +import org.jclouds.logging.Logger; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +/** + * Shows an example of how to use @{link BlobStoreContext} injected with Guice. + * + * @author Adrian Cole + */ +@Singleton +public class AddTweetsController extends HttpServlet implements + Function, List> { + + /** The serialVersionUID */ + private static final long serialVersionUID = 3888348023150822683L; + private final Map contexts; + private final ServiceToStoredTweetStatuses blobStoreContextToContainerResult; + + @Resource + protected Logger logger = Logger.NULL; + + @Inject + AddTweetsController(Map contexts, + ServiceToStoredTweetStatuses blobStoreContextToContainerResult) { + this.contexts = contexts; + this.blobStoreContextToContainerResult = blobStoreContextToContainerResult; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + addMyTweetsToRequest(request); + RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/tweets.jsp"); + dispatcher.forward(request, response); + } catch (Exception e) { + logger.error(e, "Error listing containers"); + throw new ServletException(e); + } + } + + void addMyTweetsToRequest(HttpServletRequest request) throws InterruptedException, + ExecutionException, TimeoutException { + request.setAttribute("tweets", apply(contexts.keySet())); + } + + public List apply(Set in) { + List statuses = Lists.newArrayList(); + for (Iterable list : Iterables.transform(in, + blobStoreContextToContainerResult)) { + Iterables.addAll(statuses, list); + } + return statuses; + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresController.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresController.java new file mode 100644 index 0000000000..592eaaa8bd --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresController.java @@ -0,0 +1,101 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.controller; + +import static com.google.common.base.Strings.nullToEmpty; +import static org.jclouds.demo.paas.RunnableHttpRequest.PLATFORM_REQUEST_ORIGINATOR_HEADER; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; + +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.demo.paas.reference.PaasConstants; +import org.jclouds.demo.paas.service.taskqueue.TaskQueue; +import org.jclouds.http.HttpRequest; +import org.jclouds.logging.Logger; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMultimap; + +/** + * Adds tasks to retrieve and store tweets in all registered contexts to an async + * task queue. + * + * @author Andrew Phillips + * @see StoreTweetsController + */ +@Singleton +public class EnqueueStoresController extends HttpServlet { + /** The serialVersionUID */ + private static final long serialVersionUID = 7215420527854203714L; + + private final Set contextNames; + private final TaskQueue taskQueue; + private final String baseUrl; + + @Resource + protected Logger logger = Logger.NULL; + + @Inject + public EnqueueStoresController(Map contexts, TaskQueue taskQueue, + @Named(PaasConstants.PROPERTY_PLATFORM_BASE_URL) String baseUrl) { + contextNames = contexts.keySet(); + this.taskQueue = taskQueue; + this.baseUrl = baseUrl; + } + + @VisibleForTesting + void enqueueStoreTweetTasks() { + for (String contextName : contextNames) { + logger.debug("enqueuing task to store tweets in blobstore '%s'", contextName); + taskQueue.add(taskQueue.getHttpRequestFactory().create(HttpRequest.builder() + .endpoint(URI.create(baseUrl + "/store/do")) + .headers(ImmutableMultimap.of("context", contextName)) + .method("GET").build())); + } + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + if (!nullToEmpty(request.getHeader(PLATFORM_REQUEST_ORIGINATOR_HEADER)).equals("scheduler")) { + response.sendError(401); + } + + try { + enqueueStoreTweetTasks(); + response.setContentType(MediaType.TEXT_PLAIN); + response.getWriter().println("Done!"); + } catch (Exception e) { + logger.error(e, "Error storing tweets"); + throw new ServletException(e); + } + } +} \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/StoreTweetsController.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/StoreTweetsController.java new file mode 100644 index 0000000000..948c9ff4ca --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/controller/StoreTweetsController.java @@ -0,0 +1,130 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.controller; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.nullToEmpty; +import static org.jclouds.demo.paas.RunnableHttpRequest.PLATFORM_REQUEST_ORIGINATOR_HEADER; + +import java.io.IOException; +import java.util.Map; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; + +import org.jclouds.blobstore.BlobMap; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.blobstore.domain.Blob; +import org.jclouds.demo.tweetstore.reference.TweetStoreConstants; +import org.jclouds.logging.Logger; +import org.jclouds.rest.AuthorizationException; + +import twitter4j.Status; +import twitter4j.Twitter; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; + +/** + * Grab tweets related to me and store them into blobstores + * + * @author Adrian Cole + */ +@Singleton +public class StoreTweetsController extends HttpServlet { + + private static final class StatusToBlob implements Function { + private final BlobMap map; + + private StatusToBlob(BlobMap map) { + this.map = map; + } + + public Blob apply(Status from) { + Blob to = map.blobBuilder().name(from.getId() + "").build(); + to.setPayload(from.getText()); + to.getPayload().getContentMetadata().setContentType(MediaType.TEXT_PLAIN); + to.getMetadata().getUserMetadata().put(TweetStoreConstants.SENDER_NAME, from.getUser().getScreenName()); + return to; + } + } + + /** The serialVersionUID */ + private static final long serialVersionUID = 7215420527854203714L; + + private final Map contexts; + private final Twitter client; + private final String container; + + @Resource + protected Logger logger = Logger.NULL; + + @Inject + @VisibleForTesting + public StoreTweetsController(Map contexts, + @Named(TweetStoreConstants.PROPERTY_TWEETSTORE_CONTAINER) String container, Twitter client) { + this.container = container; + this.contexts = contexts; + this.client = client; + } + + @VisibleForTesting + public void addMyTweets(String contextName, Iterable responseList) { + BlobStoreContext context = checkNotNull(contexts.get(contextName), "no context for " + contextName + " in " + + contexts.keySet()); + BlobMap map = context.createBlobMap(container); + for (Status status : responseList) { + Blob blob = null; + try { + blob = new StatusToBlob(map).apply(status); + map.put(status.getId() + "", blob); + } catch (AuthorizationException e) { + throw e; + } catch (Exception e) { + logger.error(e, "Error storing tweet %s (blob[%s]) on map %s/%s", status.getId(), blob, context, container); + } + } + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + if (nullToEmpty(request.getHeader(PLATFORM_REQUEST_ORIGINATOR_HEADER)).equals("taskqueue-twitter")) { + try { + String contextName = checkNotNull(request.getHeader("context"), "missing header context"); + logger.info("retrieving tweets"); + addMyTweets(contextName, client.getMentions()); + logger.debug("done storing tweets"); + response.setContentType(MediaType.TEXT_PLAIN); + response.getWriter().println("Done!"); + } catch (Exception e) { + logger.error(e, "Error storing tweets"); + throw new ServletException(e); + } + } else { + response.sendError(401); + } + } +} \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/domain/StoredTweetStatus.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/domain/StoredTweetStatus.java new file mode 100644 index 0000000000..42ad65df01 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/domain/StoredTweetStatus.java @@ -0,0 +1,149 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.domain; + +import java.io.Serializable; + +/** + * + * @author Adrian Cole + */ +public class StoredTweetStatus implements Comparable, Serializable { + + /** The serialVersionUID */ + private static final long serialVersionUID = -3257496189689220018L; + private final String service; + private final String host; + private final String container; + private final String id; + private final String from; + private final String tweet; + private final String status; + + @Override + public String toString() { + return "StoredTweetStatus [container=" + container + ", from=" + from + ", host=" + host + + ", id=" + id + ", service=" + service + ", status=" + status + ", tweet=" + tweet + + "]"; + } + + public StoredTweetStatus(String service, String host, String container, String id, String from, + String tweet, String status) { + this.service = service; + this.host = host; + this.container = container; + this.id = id; + this.from = from; + this.tweet = tweet; + this.status = status; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((container == null) ? 0 : container.hashCode()); + result = prime * result + ((from == null) ? 0 : from.hashCode()); + result = prime * result + ((host == null) ? 0 : host.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((service == null) ? 0 : service.hashCode()); + result = prime * result + ((tweet == null) ? 0 : tweet.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + StoredTweetStatus other = (StoredTweetStatus) obj; + if (container == null) { + if (other.container != null) + return false; + } else if (!container.equals(other.container)) + return false; + if (from == null) { + if (other.from != null) + return false; + } else if (!from.equals(other.from)) + return false; + if (host == null) { + if (other.host != null) + return false; + } else if (!host.equals(other.host)) + return false; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (service == null) { + if (other.service != null) + return false; + } else if (!service.equals(other.service)) + return false; + if (tweet == null) { + if (other.tweet != null) + return false; + } else if (!tweet.equals(other.tweet)) + return false; + return true; + } + + + public String getService() { + return service; + } + + public String getHost() { + return host; + } + + public String getContainer() { + return container; + } + + public String getFrom() { + return from; + } + + public String getTweet() { + return tweet; + } + + public String getStatus() { + return status; + } + + public int compareTo(StoredTweetStatus o) { + if (id == null) + return -1; + return (int) ((this == o) ? 0 : id.compareTo(o.id)); + } + + + public String getId() { + return id; + } + +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatus.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatus.java new file mode 100644 index 0000000000..2a6ea0a69c --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatus.java @@ -0,0 +1,71 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.functions; + +import static org.jclouds.util.Strings2.toStringAndClose; + +import javax.annotation.Resource; + +import org.jclouds.blobstore.BlobMap; +import org.jclouds.blobstore.domain.Blob; +import org.jclouds.demo.tweetstore.domain.StoredTweetStatus; +import org.jclouds.demo.tweetstore.reference.TweetStoreConstants; +import org.jclouds.logging.Logger; + +import com.google.common.base.Function; + +/** + * + * @author Adrian Cole + */ +public class KeyToStoredTweetStatus implements Function { + private final String host; + private final BlobMap map; + private final String service; + private final String container; + + @Resource + protected Logger logger = Logger.NULL; + + KeyToStoredTweetStatus(BlobMap map, String service, String host, String container) { + this.host = host; + this.map = map; + this.service = service; + this.container = container; + } + + public StoredTweetStatus apply(String id) { + String status; + String from; + String tweet; + try { + long start = System.currentTimeMillis(); + Blob blob = map.get(id); + status = ((System.currentTimeMillis() - start) + "ms"); + from = blob.getMetadata().getUserMetadata().get(TweetStoreConstants.SENDER_NAME); + tweet = toStringAndClose(blob.getPayload().getInput()); + } catch (Exception e) { + logger.error(e, "Error listing container %s//%s/%s", service, container, id); + status = (e.getMessage()); + tweet = ""; + from = ""; + } + return new StoredTweetStatus(service, host, container, id, from, tweet, status); + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatuses.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatuses.java new file mode 100644 index 0000000000..b29ec14549 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatuses.java @@ -0,0 +1,73 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.functions; + +import java.net.URI; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.jclouds.Context; +import org.jclouds.blobstore.BlobMap; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.demo.tweetstore.domain.StoredTweetStatus; +import org.jclouds.demo.tweetstore.reference.TweetStoreConstants; +import org.jclouds.logging.Logger; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; + +@Singleton +public class ServiceToStoredTweetStatuses implements Function> { + + private final Map contexts; + private final String container; + + @Inject + public ServiceToStoredTweetStatuses(Map contexts, + @Named(TweetStoreConstants.PROPERTY_TWEETSTORE_CONTAINER) String container) { + this.contexts = contexts; + this.container = container; + } + + @Resource + protected Logger logger = Logger.NULL; + + public Iterable apply(String service) { + BlobStoreContext context = contexts.get(service); + String host = URI.create(context.unwrap(Context.class).getProviderMetadata().getEndpoint()).getHost(); + try { + BlobMap blobMap = context.createBlobMap(container); + Set blobs = blobMap.keySet(); + return Iterables.transform(blobs, new KeyToStoredTweetStatus(blobMap, service, host, + container)); + } catch (Exception e) { + StoredTweetStatus result = new StoredTweetStatus(service, host, container, null, null, + null, e.getMessage()); + logger.error(e, "Error listing service %s", service); + return Collections.singletonList(result); + } + + } +} \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/reference/TweetStoreConstants.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/reference/TweetStoreConstants.java new file mode 100644 index 0000000000..42ec480ae2 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/reference/TweetStoreConstants.java @@ -0,0 +1,34 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.reference; + +/** + * Configuration properties and constants used in TweetStore connections. + * + * @author Adrian Cole + */ +public interface TweetStoreConstants { + static final String PROPERTY_TWEETSTORE_BLOBSTORES = "jclouds.tweetstore.blobstores"; + static final String PROPERTY_TWEETSTORE_CONTAINER = "jclouds.tweetstore.container"; + /** + * Note that this has to conform to restrictions of all blobstores. for + * example, azure doesn't support periods. + */ + static final String SENDER_NAME = "sendername"; +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/reference/TwitterConstants.java b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/reference/TwitterConstants.java new file mode 100644 index 0000000000..dc8b97915f --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/java/org/jclouds/demo/tweetstore/reference/TwitterConstants.java @@ -0,0 +1,31 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.reference; + +/** + * Configuration properties and constants used in Twitter connections. + * + * @author Andrew Phillips + */ +public interface TwitterConstants { + static final String PROPERTY_TWITTER_CONSUMER_KEY = "twitter.consumer.identity"; + static final String PROPERTY_TWITTER_CONSUMER_SECRET = "twitter.consumer.credential"; + static final String PROPERTY_TWITTER_ACCESSTOKEN = "twitter.access.identity"; + static final String PROPERTY_TWITTER_ACCESSTOKEN_SECRET = "twitter.access.credential"; +} diff --git a/demos/tweetstore/heroku-tweetstore/src/main/platform/.gitignore b/demos/tweetstore/heroku-tweetstore/src/main/platform/.gitignore new file mode 100644 index 0000000000..843dfe79c0 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/platform/.gitignore @@ -0,0 +1 @@ +# PaaS vendor specific files go in here \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/main/resources/jobs.xml b/demos/tweetstore/heroku-tweetstore/src/main/resources/jobs.xml new file mode 100644 index 0000000000..b740fdd52f --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/resources/jobs.xml @@ -0,0 +1,29 @@ + + + + + + enqueue-store-tweet-tasks + Enqueue 'store tweet' tasks for all contexts + org.jclouds.demo.paas.service.scheduler.HttpRequestJob + + + url + /stores/do + + + + + + + submit-recurring-job + enqueue-store-tweet-tasks + 10 + MINUTE + + + + \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/main/resources/quartz.properties b/demos/tweetstore/heroku-tweetstore/src/main/resources/quartz.properties new file mode 100644 index 0000000000..12a0fcfe91 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/resources/quartz.properties @@ -0,0 +1,28 @@ +#============================================================================ +# Configure Main Scheduler Properties +#============================================================================ + +org.quartz.scheduler.skipUpdateCheck: true + +#============================================================================ +# Configure ThreadPool +#============================================================================ + +org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool +org.quartz.threadPool.threadCount: 1 + +#============================================================================ +# Configure JobStore +#============================================================================ + +org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore + +#============================================================================ +# Configure the Job Initialization Plugin +#============================================================================ + +org.quartz.plugin.jobInitializer.class: org.jclouds.demo.paas.service.scheduler.quartz.plugins.TransactionlessXmlSchedulingDataProcessorPlugin +org.quartz.plugin.jobInitializer.fileNames: jobs.xml +org.quartz.plugin.jobInitializer.failOnFileNotFound: true +org.quartz.plugin.jobInitializer.scanInterval: 0 +#org.quartz.plugin.jobInitializer.wrapInUserTransaction: false \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/main/webapp/WEB-INF/web.xml b/demos/tweetstore/heroku-tweetstore/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..9482bd9d9e --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,60 @@ + + + + jclouds-tweetstore + + + quartz:scheduler-context-servlet-context-key + servlet-context + + + + + guiceFilter + com.google.inject.servlet.GuiceFilter + + + + guiceFilter + /* + + + + + org.jclouds.demo.paas.config.PlatformServicesInitializer + + + + org.quartz.ee.servlet.QuartzInitializerListener + + + + org.jclouds.demo.tweetstore.config.GuiceServletConfig + + + + index.jsp + + + \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/main/webapp/index.jsp b/demos/tweetstore/heroku-tweetstore/src/main/webapp/index.jsp new file mode 100644 index 0000000000..d8d1724bcf --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/webapp/index.jsp @@ -0,0 +1,31 @@ +<%-- + + Licensed to jclouds, Inc. (jclouds) under one or more + contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. jclouds licenses this file + to you 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. + +--%> + + +jclouds: anyweight cloudware for java + + +

Welcome!

+

Click here to see tweets about jclouds.

+

+ + diff --git a/demos/tweetstore/heroku-tweetstore/src/main/webapp/tweets.jsp b/demos/tweetstore/heroku-tweetstore/src/main/webapp/tweets.jsp new file mode 100644 index 0000000000..1b30f7ea11 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/main/webapp/tweets.jsp @@ -0,0 +1,109 @@ +<%-- + + Licensed to jclouds, Inc. (jclouds) under one or more + contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. jclouds licenses this file + to you 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 buffer="20kb"%> +<%@ taglib uri="http://displaytag.sf.net" prefix="display"%> + + +jclouds: anyweight cloudware for java + + + +

Tweets in Clouds

+ + + + + + + +
+
+ + + + + + + + +
+
+ + diff --git a/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollectorTest.java b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollectorTest.java new file mode 100644 index 0000000000..031bb199fc --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollectorTest.java @@ -0,0 +1,82 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.config.util; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.util.Map; +import java.util.Properties; + +import org.jclouds.demo.tweetstore.config.util.CredentialsCollector; +import org.jclouds.demo.tweetstore.config.util.CredentialsCollector.Credential; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; + +/** + * Tests behavior of {@code CredentialsCollector} + * + * @author Andrew Phillips + */ +@Test(groups = "unit") +public class CredentialsCollectorTest { + private CredentialsCollector collector = new CredentialsCollector(); + + public void testEmptyProperties() { + assertTrue(collector.apply(new Properties()).isEmpty(), + "Expected returned map to be empty"); + } + + public void testNoCredentials() { + Properties properties = propertiesOf(ImmutableMap.of("not-an-identity", + "v1", "not-a-credential", "v2")); + assertTrue(collector.apply(properties).isEmpty(), + "Expected returned map to be empty"); + } + + private static Properties propertiesOf(Map entries) { + Properties properties = new Properties(); + properties.putAll(entries); + return properties; + } + + public void testNonMatchingCredentials() { + Properties properties = propertiesOf(ImmutableMap.of("non_matching.identity", "v1", + "non_matching.credential", "v2")); + assertTrue(collector.apply(properties).isEmpty(), + "Expected returned map to be empty"); + } + + public void testIncompleteCredentials() { + Properties properties = propertiesOf(ImmutableMap.of("acme.identity", "v1", + "acme-2.credential", "v2")); + assertTrue(collector.apply(properties).isEmpty(), + "Expected returned map to be empty"); + } + + public void testCredentials() { + Properties properties = propertiesOf(ImmutableMap.of("acme.identity", "v1", + "acme.credential", "v2", "acme-2.identity", "v3", + "acme-2.credential", "v4")); + assertEquals(collector.apply(properties), + ImmutableMap.of("acme", new Credential("v1", "v2"), + "acme-2", new Credential("v3", "v4"))); + } +} \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/AddTweetsControllerTest.java b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/AddTweetsControllerTest.java new file mode 100644 index 0000000000..1c93403650 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/AddTweetsControllerTest.java @@ -0,0 +1,78 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.controller; + +import static org.testng.Assert.assertEquals; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import org.jclouds.ContextBuilder; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.blobstore.TransientApiMetadata; +import org.jclouds.blobstore.domain.Blob; +import org.jclouds.demo.tweetstore.domain.StoredTweetStatus; +import org.jclouds.demo.tweetstore.functions.ServiceToStoredTweetStatuses; +import org.jclouds.demo.tweetstore.reference.TweetStoreConstants; +import org.testng.annotations.Test; +import org.testng.collections.Maps; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * Tests behavior of {@code AddTweetsController} + * + * @author Adrian Cole + */ +@Test(groups = "unit") +public class AddTweetsControllerTest { + + Map createServices(String container) throws InterruptedException, + ExecutionException { + Map services = Maps.newHashMap(); + TransientApiMetadata transientApiMetadata = TransientApiMetadata.builder().build(); + for (String name : new String[] { "1", "2" }) { + BlobStoreContext context = ContextBuilder.newBuilder(transientApiMetadata).build(BlobStoreContext.class); + context.getAsyncBlobStore().createContainerInLocation(null, container).get(); + Blob blob = context.getAsyncBlobStore().blobBuilder("1").build(); + blob.getMetadata().getUserMetadata().put(TweetStoreConstants.SENDER_NAME, "frank"); + blob.setPayload("I love beans!"); + context.getAsyncBlobStore().putBlob(container, blob).get(); + services.put(name, context); + } + return services; + } + + public void testStoreTweets() throws IOException, InterruptedException, ExecutionException { + String container = "container"; + Map contexts = createServices(container); + + ServiceToStoredTweetStatuses function = new ServiceToStoredTweetStatuses(contexts, container); + AddTweetsController controller = new AddTweetsController(contexts, function); + List list = controller.apply(ImmutableSet.of("1", "2")); + assertEquals(list.size(), 2); + assertEquals(list, ImmutableList.of(new StoredTweetStatus("1", "localhost", container, "1", + "frank", "I love beans!", null), new StoredTweetStatus("2", "localhost", container, + "1", "frank", "I love beans!", null))); + + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresControllerTest.java b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresControllerTest.java new file mode 100644 index 0000000000..3c5e5b1d80 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresControllerTest.java @@ -0,0 +1,85 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.controller; + +import static org.easymock.EasyMock.*; + +import java.net.URI; +import java.util.Map; + +import org.jclouds.ContextBuilder; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.blobstore.TransientApiMetadata; +import org.jclouds.demo.paas.RunnableHttpRequest; +import org.jclouds.demo.paas.RunnableHttpRequest.Factory; +import org.jclouds.demo.paas.service.taskqueue.TaskQueue; +import org.jclouds.http.HttpRequest; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; + +/** + * Tests behavior of {@code EnqueueStoresController} + * + * @author Andrew Phillips + */ +@Test(groups = "unit") +public class EnqueueStoresControllerTest { + + Map createBlobStores() { + TransientApiMetadata transientApiMetadata = TransientApiMetadata.builder().build(); + Map contexts = ImmutableMap.of( + "test1", ContextBuilder.newBuilder(transientApiMetadata).build(BlobStoreContext.class), + "test2", ContextBuilder.newBuilder(transientApiMetadata).build(BlobStoreContext.class)); + return contexts; + } + + public void testEnqueueStores() { + Map stores = createBlobStores(); + TaskQueue taskQueue = createMock(TaskQueue.class); + Factory httpRequestFactory = createMock(Factory.class); + EnqueueStoresController function = new EnqueueStoresController(stores, + taskQueue, "http://localhost:8080"); + + expect(taskQueue.getHttpRequestFactory()).andStubReturn(httpRequestFactory); + + HttpRequest storeInTest1Request = HttpRequest.builder().endpoint( + URI.create("http://localhost:8080/store/do")) + .headers(ImmutableMultimap.of("context", "test1")).method("GET").build(); + RunnableHttpRequest storeInTest1Task = null; + expect(httpRequestFactory.create(eq(storeInTest1Request))).andReturn(storeInTest1Task); + + HttpRequest storeInTest2Request = HttpRequest.builder().endpoint( + URI.create("http://localhost:8080/store/do")) + .headers(ImmutableMultimap.of("context", "test2")).method("GET").build(); + RunnableHttpRequest storeInTest2Task = null; + expect(httpRequestFactory.create(eq(storeInTest2Request))).andReturn(storeInTest2Task); + + taskQueue.add(storeInTest1Task); + expectLastCall(); + taskQueue.add(storeInTest2Task); + expectLastCall(); + replay(httpRequestFactory, taskQueue); + + function.enqueueStoreTweetTasks(); + + verify(taskQueue); + } +} \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/StoreTweetsControllerTest.java b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/StoreTweetsControllerTest.java new file mode 100644 index 0000000000..9cc56351f2 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/controller/StoreTweetsControllerTest.java @@ -0,0 +1,120 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.controller; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.jclouds.util.Strings2.toStringAndClose; +import static org.testng.Assert.assertEquals; + +import java.io.IOException; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ExecutionException; + +import org.jclouds.ContextBuilder; +import org.jclouds.blobstore.BlobMap; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.blobstore.TransientApiMetadata; +import org.jclouds.blobstore.domain.Blob; +import org.jclouds.demo.tweetstore.reference.TweetStoreConstants; +import org.testng.annotations.Test; + +import twitter4j.Status; +import twitter4j.Twitter; +import twitter4j.User; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +/** + * Tests behavior of {@code StoreTweetsController} + * + * @author Adrian Cole + */ +@Test(groups = "unit") +public class StoreTweetsControllerTest { + + Twitter createTwitter() { + return createMock(Twitter.class); + } + + Map createBlobStores() throws InterruptedException, ExecutionException { + TransientApiMetadata transientApiMetadata = TransientApiMetadata.builder().build(); + Map contexts = ImmutableMap.of( + "test1", ContextBuilder.newBuilder(transientApiMetadata).build(BlobStoreContext.class), + "test2", ContextBuilder.newBuilder(transientApiMetadata).build(BlobStoreContext.class)); + for (BlobStoreContext blobstore : contexts.values()) { + blobstore.getAsyncBlobStore().createContainerInLocation(null, "favo").get(); + } + return contexts; + } + + public void testStoreTweets() throws IOException, InterruptedException, ExecutionException { + Map stores = createBlobStores(); + StoreTweetsController function = new StoreTweetsController(stores, "favo", createTwitter()); + + User frank = createMock(User.class); + expect(frank.getScreenName()).andReturn("frank").atLeastOnce(); + + Status frankStatus = createMock(Status.class); + expect(frankStatus.getId()).andReturn(1l).atLeastOnce(); + expect(frankStatus.getUser()).andReturn(frank).atLeastOnce(); + expect(frankStatus.getText()).andReturn("I love beans!").atLeastOnce(); + + User jimmy = createMock(User.class); + expect(jimmy.getScreenName()).andReturn("jimmy").atLeastOnce(); + + Status jimmyStatus = createMock(Status.class); + expect(jimmyStatus.getId()).andReturn(2l).atLeastOnce(); + expect(jimmyStatus.getUser()).andReturn(jimmy).atLeastOnce(); + expect(jimmyStatus.getText()).andReturn("cloud is king").atLeastOnce(); + + replay(frank); + replay(frankStatus); + replay(jimmy); + replay(jimmyStatus); + + function.addMyTweets("test1", ImmutableList.of(frankStatus, jimmyStatus)); + function.addMyTweets("test2", ImmutableList.of(frankStatus, jimmyStatus)); + + verify(frank); + verify(frankStatus); + verify(jimmy); + verify(jimmyStatus); + + for (Entry entry : stores.entrySet()) { + BlobMap map = entry.getValue().createBlobMap("favo"); + Blob frankBlob = map.get("1"); + assertEquals(frankBlob.getMetadata().getName(), "1"); + assertEquals(frankBlob.getMetadata().getUserMetadata().get(TweetStoreConstants.SENDER_NAME), "frank"); + assertEquals(frankBlob.getMetadata().getContentMetadata().getContentType(), "text/plain"); + assertEquals(toStringAndClose(frankBlob.getPayload().getInput()), "I love beans!"); + + Blob jimmyBlob = map.get("2"); + assertEquals(jimmyBlob.getMetadata().getName(), "2"); + assertEquals(jimmyBlob.getMetadata().getUserMetadata().get(TweetStoreConstants.SENDER_NAME), "jimmy"); + assertEquals(jimmyBlob.getMetadata().getContentMetadata().getContentType(), "text/plain"); + assertEquals(toStringAndClose(jimmyBlob.getPayload().getInput()), "cloud is king"); + } + + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatusTest.java b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatusTest.java new file mode 100644 index 0000000000..aab06ec0b6 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatusTest.java @@ -0,0 +1,69 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.functions; + +import static org.testng.Assert.assertEquals; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +import org.jclouds.ContextBuilder; +import org.jclouds.blobstore.BlobMap; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.blobstore.TransientApiMetadata; +import org.jclouds.blobstore.domain.Blob; +import org.jclouds.demo.tweetstore.domain.StoredTweetStatus; +import org.jclouds.demo.tweetstore.reference.TweetStoreConstants; +import org.testng.annotations.Test; + +/** + * Tests behavior of {@code KeyToStoredTweetStatus} + * + * @author Adrian Cole + */ +@Test(groups = "unit") +public class KeyToStoredTweetStatusTest { + + BlobMap createMap() throws InterruptedException, ExecutionException { + BlobStoreContext context = + ContextBuilder.newBuilder(TransientApiMetadata.builder().build()).build(BlobStoreContext.class); + context.getBlobStore().createContainerInLocation(null, "test1"); + return context.createBlobMap("test1"); + } + + public void testStoreTweets() throws IOException, InterruptedException, ExecutionException { + BlobMap map = createMap(); + Blob blob = map.blobBuilder().name("1").build(); + blob.getMetadata().getUserMetadata().put(TweetStoreConstants.SENDER_NAME, "frank"); + blob.setPayload("I love beans!"); + map.put("1", blob); + String host = "localhost"; + String service = "stub"; + String container = "tweetstore"; + + KeyToStoredTweetStatus function = new KeyToStoredTweetStatus(map, service, host, container); + StoredTweetStatus result = function.apply("1"); + + StoredTweetStatus expected = new StoredTweetStatus(service, host, container, "1", "frank", + "I love beans!", null); + + assertEquals(result, expected); + + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatusesTest.java b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatusesTest.java new file mode 100644 index 0000000000..5fec52711e --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatusesTest.java @@ -0,0 +1,75 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.functions; + +import static org.testng.Assert.assertEquals; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import org.jclouds.ContextBuilder; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.blobstore.TransientApiMetadata; +import org.jclouds.blobstore.domain.Blob; +import org.jclouds.demo.tweetstore.domain.StoredTweetStatus; +import org.jclouds.demo.tweetstore.reference.TweetStoreConstants; +import org.testng.annotations.Test; +import org.testng.collections.Maps; + +import com.google.common.collect.Iterables; + +/** + * Tests behavior of {@code ServiceToStoredTweetStatuses} + * + * @author Adrian Cole + */ +@Test(groups = "unit") +public class ServiceToStoredTweetStatusesTest { + + Map createServices(String container) throws InterruptedException, + ExecutionException { + Map services = Maps.newHashMap(); + TransientApiMetadata transientApiMetadata = TransientApiMetadata.builder().build(); + for (String name : new String[] { "1", "2" }) { + BlobStoreContext context = ContextBuilder.newBuilder(transientApiMetadata).build(BlobStoreContext.class); + context.getAsyncBlobStore().createContainerInLocation(null, container).get(); + Blob blob = context.getAsyncBlobStore().blobBuilder("1").build(); + blob.getMetadata().getUserMetadata().put(TweetStoreConstants.SENDER_NAME, "frank"); + blob.setPayload("I love beans!"); + context.getAsyncBlobStore().putBlob(container, blob).get(); + services.put(name, context); + } + return services; + } + + public void testStoreTweets() throws IOException, InterruptedException, ExecutionException { + String container = "container"; + Map contexts = createServices(container); + + ServiceToStoredTweetStatuses function = new ServiceToStoredTweetStatuses(contexts, container); + + assertEquals(Iterables.getLast(function.apply("1")), new StoredTweetStatus("1", "localhost", + container, "1", "frank", "I love beans!", null)); + + assertEquals(Iterables.getLast(function.apply("2")), new StoredTweetStatus("2", "localhost", + container, "1", "frank", "I love beans!", null)); + + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/JettyServer.java b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/JettyServer.java new file mode 100644 index 0000000000..d358287781 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/JettyServer.java @@ -0,0 +1,68 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.integration; + +import static com.google.common.io.Closeables.closeQuietly; +import static java.lang.String.format; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import javax.servlet.ServletException; + +/** + * Basic functionality to start a local WAR-supporting Jetty instance. + * + * @author Andrew Phillips + */ +public class JettyServer { + protected Runner2 server; + + public void writePropertiesAndStartServer(final String port, final String warfile, + Properties props) throws IOException, InterruptedException, ServletException { + String filename = String.format( + "%1$s/WEB-INF/jclouds.properties", warfile); + System.err.println("file: " + filename); + storeProperties(filename, props); + assert new File(filename).exists(); + // Jetty uses SLF4J by default + System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.JavaUtilLog"); + System.setProperty("java.util.logging.config.file", + format("%s/WEB-INF/logging.properties", warfile)); + server = Runner2.createRunner(new String[] { "--port", port, warfile }); + server.start(); + TimeUnit.SECONDS.sleep(30); + } + + private static void storeProperties(String filename, Properties props) throws IOException { + FileOutputStream targetFile = new FileOutputStream(filename); + try { + props.store(targetFile, "test"); + } finally { + closeQuietly(targetFile); + } + } + + public void stop() { + server.stop(); + } +} \ No newline at end of file diff --git a/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/Runner2.java b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/Runner2.java new file mode 100644 index 0000000000..feb9e2c408 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/Runner2.java @@ -0,0 +1,72 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.integration; + +import javax.servlet.ServletException; + +import org.mortbay.jetty.runner.Runner; + +/** + * @see Runner + * @author Andrew Phillips + */ +class Runner2 extends Runner { + public static Runner2 createRunner(String[] args) throws ServletException { + Runner2 runner = new Runner2(); + try { + runner.configure(args); + } catch (Exception exception) { + throw new ServletException("Unable to configure runner", exception); + } + return runner; + } + + private final Thread serverThread; + + private Runner2() { + serverThread = new Thread(new Runnable() { + public void run() { + try { + Runner2.this.run(); + } catch (Exception exception) { + System.err.println("exception starting server: " + exception); + } + } + }); + } + + void start() throws ServletException { + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + public void run() { + stop(); + } + })); + serverThread.start(); + } + + void stop() { + try { + _server.stop(); + } catch (Exception exception) { + System.err.println("exception stopping server: " + exception); + } + serverThread.interrupt(); + } + +} diff --git a/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/TweetStoreLiveTest.java b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/TweetStoreLiveTest.java new file mode 100644 index 0000000000..71e483b44b --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/TweetStoreLiveTest.java @@ -0,0 +1,237 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.integration; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.jclouds.demo.paas.RunnableHttpRequest.PLATFORM_REQUEST_ORIGINATOR_HEADER; +import static org.jclouds.demo.tweetstore.reference.TweetStoreConstants.PROPERTY_TWEETSTORE_BLOBSTORES; +import static org.jclouds.demo.tweetstore.reference.TweetStoreConstants.PROPERTY_TWEETSTORE_CONTAINER; +import static org.jclouds.demo.tweetstore.reference.TwitterConstants.PROPERTY_TWITTER_ACCESSTOKEN; +import static org.jclouds.demo.tweetstore.reference.TwitterConstants.PROPERTY_TWITTER_ACCESSTOKEN_SECRET; +import static org.jclouds.demo.tweetstore.reference.TwitterConstants.PROPERTY_TWITTER_CONSUMER_KEY; +import static org.jclouds.demo.tweetstore.reference.TwitterConstants.PROPERTY_TWITTER_CONSUMER_SECRET; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.jclouds.Context; +import org.jclouds.ContextBuilder; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.demo.tweetstore.controller.StoreTweetsController; +import org.jclouds.logging.log4j.config.Log4JLoggingModule; +import org.jclouds.rest.AuthorizationException; +import org.jclouds.util.Strings2; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import twitter4j.ResponseList; +import twitter4j.Status; +import twitter4j.Twitter; +import twitter4j.TwitterException; +import twitter4j.TwitterFactory; +import twitter4j.conf.Configuration; +import twitter4j.conf.ConfigurationBuilder; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.inject.Module; + +/** + * Starts up the RUN@cloud for Java Development environment and deploys an application which + * tests accesses twitter and blobstores. + * + * @author Andrew Phillips + */ +@Test(groups = "live", singleThreaded = true) +public class TweetStoreLiveTest { + + JettyServer server; + private URL url; + private Map contexts; + private String container; + private static final Iterable blobstores = + Splitter.on(',').split(getRequiredSystemProperty(PROPERTY_TWEETSTORE_BLOBSTORES)); + private static final Properties props = new Properties(); + + @BeforeTest + void clearAndCreateContainers() throws InterruptedException, ExecutionException, TimeoutException, IOException, + TwitterException { + container = getRequiredSystemProperty(PROPERTY_TWEETSTORE_CONTAINER); + + props.setProperty(PROPERTY_TWEETSTORE_CONTAINER, container); + + // put all identity/credential pairs into the client + addCredentialsForBlobStores(props); + + // example of an ad-hoc client configuration + addConfigurationForTwitter(props); + + // for testing, capture logs. + final Set wiring = ImmutableSet. of(new Log4JLoggingModule()); + this.contexts = Maps.newConcurrentMap(); + + for (String provider : blobstores) { + contexts.put(provider, ContextBuilder.newBuilder(provider) + .modules(wiring).overrides(props).build(BlobStoreContext.class)); + } + + Configuration conf = new ConfigurationBuilder() + .setOAuthConsumerKey(props.getProperty(PROPERTY_TWITTER_CONSUMER_KEY)) + .setOAuthConsumerSecret(props.getProperty(PROPERTY_TWITTER_CONSUMER_SECRET)) + .setOAuthAccessToken(props.getProperty(PROPERTY_TWITTER_ACCESSTOKEN)) + .setOAuthAccessTokenSecret(props.getProperty(PROPERTY_TWITTER_ACCESSTOKEN_SECRET)) + .build(); + Twitter client = new TwitterFactory(conf).getInstance(); + StoreTweetsController controller = new StoreTweetsController(contexts, container, client); + + ResponseList statuses = client.getMentions(); + + boolean deleted = false; + for (BlobStoreContext context : contexts.values()) { + try { + if (context.getBlobStore().containerExists(container)) { + System.err.printf("deleting container %s at %s%n", container, + context.unwrap(Context.class).getProviderMetadata().getEndpoint()); + context.getBlobStore().deleteContainer(container); + deleted = true; + } + } catch (AuthorizationException e) { + throw new AuthorizationException("for context: " + context, e); + } + } + if (deleted) { + System.err.println("sleeping 60 seconds to allow containers to clear"); + Thread.sleep(60000); + } + for (BlobStoreContext context : contexts.values()) { + System.err.printf("creating container %s at %s%n", container, + context.unwrap(Context.class).getProviderMetadata().getEndpoint()); + context.getBlobStore().createContainerInLocation(null, container); + } + + if (deleted) { + System.err.println("sleeping 5 seconds to allow containers to create"); + Thread.sleep(5000); + } + + for (Entry entry : contexts.entrySet()) { + System.err.printf("filling container %s at %s%n", container, entry.getKey()); + controller.addMyTweets(entry.getKey(), statuses); + } + } + + private static String getRequiredSystemProperty(String key) { + return checkNotNull(System.getProperty(key), key); + } + + private void addConfigurationForTwitter(Properties props) { + props.setProperty(PROPERTY_TWITTER_CONSUMER_KEY, + getRequiredSystemProperty("test." + PROPERTY_TWITTER_CONSUMER_KEY)); + props.setProperty(PROPERTY_TWITTER_CONSUMER_SECRET, + getRequiredSystemProperty("test." + PROPERTY_TWITTER_CONSUMER_SECRET)); + props.setProperty(PROPERTY_TWITTER_ACCESSTOKEN, + getRequiredSystemProperty("test." + PROPERTY_TWITTER_ACCESSTOKEN)); + props.setProperty(PROPERTY_TWITTER_ACCESSTOKEN_SECRET, + getRequiredSystemProperty("test." + PROPERTY_TWITTER_ACCESSTOKEN_SECRET)); + } + + private void addCredentialsForBlobStores(Properties props) { + for (String provider : blobstores) { + props.setProperty(provider + ".identity", + getRequiredSystemProperty("test." + provider + ".identity")); + props.setProperty(provider + ".credential", + getRequiredSystemProperty("test." + provider + ".credential")); + } + } + + @BeforeTest(dependsOnMethods = "clearAndCreateContainers") + @Parameters({ "warfile", "jetty.address", "jetty.port" }) + public void startDevAppServer(final String warfile, final String address, final String port) throws Exception { + url = new URL(String.format("http://%s:%s", address, port)); + + server = new JettyServer(); + server.writePropertiesAndStartServer(port, warfile, props); + } + + @Test + public void shouldPass() throws InterruptedException, IOException { + InputStream i = url.openStream(); + String string = Strings2.toStringAndClose(i); + assert string.indexOf("Welcome") >= 0 : string; + } + + @Test(dependsOnMethods = "shouldPass", expectedExceptions = IOException.class) + public void shouldFail() throws InterruptedException, IOException { + new URL(url, "/store/do").openStream(); + } + + @Test(dependsOnMethods = "shouldFail") + public void testPrimeContainers() throws IOException, InterruptedException { + URL gurl = new URL(url, "/store/do"); + + for (String context : blobstores) { + System.out.println("storing at context: " + context); + HttpURLConnection connection = (HttpURLConnection) gurl.openConnection(); + connection.addRequestProperty(PLATFORM_REQUEST_ORIGINATOR_HEADER, "taskqueue-twitter"); + connection.addRequestProperty("context", context); + InputStream i = connection.getInputStream(); + String string = Strings2.toStringAndClose(i); + assert string.indexOf("Done!") >= 0 : string; + connection.disconnect(); + } + + System.err.println("sleeping 20 seconds to allow for eventual consistency delay"); + Thread.sleep(20000); + for (BlobStoreContext context : contexts.values()) { + assert context.createInputStreamMap(container).size() > 0 : context.unwrap(Context.class).getProviderMetadata().getEndpoint(); + } + } + + @Test(invocationCount = 5, dependsOnMethods = "testPrimeContainers") + public void testSerial() throws InterruptedException, IOException { + URL gurl = new URL(url, "/tweets/get"); + InputStream i = gurl.openStream(); + String string = Strings2.toStringAndClose(i); + assert string.indexOf("Tweets in Clouds") >= 0 : string; + } + + @Test(invocationCount = 10, dependsOnMethods = "testPrimeContainers", threadPoolSize = 3) + public void testParallel() throws InterruptedException, IOException { + URL gurl = new URL(url, "/tweets/get"); + InputStream i = gurl.openStream(); + String string = Strings2.toStringAndClose(i); + assert string.indexOf("Tweets in Clouds") >= 0 : string; + } + + @AfterTest + public void stopDevAppServer() throws Exception { + server.stop(); + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/util/ObjectFields.java b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/util/ObjectFields.java new file mode 100644 index 0000000000..cc7c696c55 --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/test/java/org/jclouds/demo/tweetstore/integration/util/ObjectFields.java @@ -0,0 +1,56 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.demo.tweetstore.integration.util; + +import java.lang.reflect.Field; + +public class ObjectFields { + + public static Object valueOf(String fieldName, Object source) { + return valueOf(fieldName, source, source.getClass()); + } + + public static Object valueOf(String fieldName, Object source, + Class fieldDeclaringClass) { + try { + return getAccessibleField(fieldName, fieldDeclaringClass).get(source); + } catch (Exception exception) { + throw new IllegalArgumentException(exception); + } + } + + private static Field getAccessibleField(String name, Class declaringClass) throws SecurityException, NoSuchFieldException { + Field field = declaringClass.getDeclaredField(name); + field.setAccessible(true); + return field; + } + + public static void set(String fieldName, Object target, Object value) { + set(fieldName, target, value, target.getClass()); + } + + public static void set(String fieldName, Object target, Object value, + Class fieldDeclaringClass) { + try { + getAccessibleField(fieldName, fieldDeclaringClass).set(target, value); + } catch (Exception exception) { + throw new IllegalArgumentException(exception); + } + } +} diff --git a/demos/tweetstore/heroku-tweetstore/src/test/resources/log4j.xml b/demos/tweetstore/heroku-tweetstore/src/test/resources/log4j.xml new file mode 100644 index 0000000000..2e5d01fb9e --- /dev/null +++ b/demos/tweetstore/heroku-tweetstore/src/test/resources/log4j.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/tweetstore/pom.xml b/demos/tweetstore/pom.xml index 22255d6eec..08d3e44a40 100644 --- a/demos/tweetstore/pom.xml +++ b/demos/tweetstore/pom.xml @@ -31,6 +31,7 @@ jclouds TweetStore demos project cf-tweetstore-spring + heroku-tweetstore gae-tweetstore gae-tweetstore-spring runatcloud-tweetstore