diff --git a/demos/tweetstore/cf-tweetstore-spring/README.txt b/demos/tweetstore/cf-tweetstore-spring/README.txt new file mode 100644 index 0000000000..c2af578674 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/pom.xml b/demos/tweetstore/cf-tweetstore-spring/pom.xml new file mode 100644 index 0000000000..657c2a972b --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/pom.xml @@ -0,0 +1,184 @@ + + + + 4.0.0 + + org.jclouds + jclouds-demos-tweetstore-project + 1.5.0-SNAPSHOT + + jclouds-demo-cf-tweetstore-spring + war + jclouds TweetStore for Cloud Foundry + jclouds TweetStore for Cloud Foundry using Spring for Dependency Injection + + + 0.8.1 + jclouds-tweetstore + http://api.cloudfoundry.com + test-${cloudfoundry.applicationid}.cloudfoundry.com + 80 + jclouds-cf-tweetstore-spring + + + + + + org.springframework + spring-context + 3.0.5.RELEASE + + + org.springframework + spring-webmvc + 3.0.5.RELEASE + + + cglib + cglib-nodep + 2.2 + runtime + + + + org.quartz-scheduler + quartz + 2.1.3 + + + org.slf4j + slf4j-api + + + + + + + org.cloudfoundry + cloudfoundry-runtime + ${cloudfoundry.version} + + + org.cloudfoundry + cloudfoundry-client-lib + 0.7.1 + test + + + org.codehaus.plexus + plexus-archiver + 2.1.1 + test + + + + + + org.springframework.maven.milestone + http://maven.springframework.org/milestone + + false + + + + + + + live + + + + maven-surefire-plugin + + + integration + integration-test + + test + + + + ${test.twitter.gae-tweetstore-spring.consumer.identity} + ${test.twitter.gae-tweetstore-spring.consumer.credential} + ${test.twitter.gae-tweetstore-spring.access.identity} + ${test.twitter.gae-tweetstore-spring.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.cloudfoundry.address} + ${test.cloudfoundry.port} + ${test.cloudfoundry.target} + ${cloudfoundry.username} + ${cloudfoundry.password} + ${jclouds.tweetstore.blobstores} + test.${jclouds.tweetstore.container} + ${project.build.directory}/${project.build.finalName} + + + + + + + + + + + deploy + + + cf-tweetstore-spring + + + + org.springframework.maven.milestone + http://maven.springframework.org/milestone + + false + + + + + + + org.cloudfoundry + cf-maven-plugin + 1.0.0.M1 + + http://api.cloudfoundry.com + ${cloudfoundry.username} + ${cloudfoundry.password} + ${cloudfoundry.applicationid} + 256 + + + + + + + diff --git a/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/PlatformServices.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/PlatformServices.java new file mode 100644 index 0000000000..0997005157 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/RunnableHttpRequest.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/RunnableHttpRequest.java new file mode 100644 index 0000000000..ad72a1a58d --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/config/PlatformServicesInitializer.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/config/PlatformServicesInitializer.java new file mode 100644 index 0000000000..f034b2a3e4 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/config/PlatformServicesInitializer.java @@ -0,0 +1,104 @@ +/** + * 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 java.util.concurrent.TimeUnit.SECONDS; + +import java.util.Properties; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.ws.rs.core.UriBuilder; + +import org.cloudfoundry.runtime.env.ApplicationInstanceInfo; +import org.cloudfoundry.runtime.env.CloudEnvironment; +import org.jclouds.PropertiesBuilder; +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.demo.tweetstore.config.util.PropertiesLoader; +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.AbstractModule; +import com.google.inject.Guice; +import com.sun.jersey.api.uri.UriBuilderImpl; + +/** + * @author Andrew Phillips + */ +public class PlatformServicesInitializer implements ServletContextListener { + public static final String PLATFORM_SERVICES_ATTRIBUTE_NAME = PlatformServices.class.getName(); + + @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 AbstractModule() { + @Override + protected void configure() { + // URL connection defaults + Properties toBind = new PropertiesBuilder().build(); + toBind.putAll(checkNotNull(new PropertiesLoader(context).get(), "properties")); + toBind.putAll(System.getProperties()); + bindProperties(binder(), toBind); + bind(UriBuilder.class).to(UriBuilderImpl.class); + } + }).getInstance(HttpCommandExecutorService.class); + } + + protected static String getBaseUrl(ServletContext context) { + ApplicationInstanceInfo instanceInfo = new CloudEnvironment().getInstanceInfo(); + return "http://" + checkNotNull(instanceInfo.getHost(), "instanceInfo.getHost()") + + ":" + instanceInfo.getPort() + 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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/reference/PaasConstants.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/reference/PaasConstants.java new file mode 100644 index 0000000000..8af7021bd2 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/service/scheduler/HttpRequestJob.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/service/scheduler/HttpRequestJob.java new file mode 100644 index 0000000000..902f5fe356 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/service/scheduler/Scheduler.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/service/scheduler/Scheduler.java new file mode 100644 index 0000000000..dabdff877b --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/service/scheduler/quartz/plugins/TransactionlessXmlSchedulingDataProcessorPlugin.java b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/service/taskqueue/TaskQueue.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/paas/service/taskqueue/TaskQueue.java new file mode 100644 index 0000000000..e317a305cf --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/DelegatingAutowireCapableBeanFactory.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/DelegatingAutowireCapableBeanFactory.java new file mode 100644 index 0000000000..7fda1bb2f0 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/DelegatingAutowireCapableBeanFactory.java @@ -0,0 +1,129 @@ +package org.jclouds.demo.tweetstore.config; + +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; + +class DelegatingAutowireCapableBeanFactory implements AutowireCapableBeanFactory { + private final AutowireCapableBeanFactory delegate; + + DelegatingAutowireCapableBeanFactory(AutowireCapableBeanFactory delegate) { + this.delegate = delegate; + } + + public T createBean(Class beanClass) throws BeansException { + return delegate.createBean(beanClass); + } + + public void autowireBean(Object existingBean) throws BeansException { + delegate.autowireBean(existingBean); + } + + public Object configureBean(Object existingBean, String beanName) + throws BeansException { + return delegate.configureBean(existingBean, beanName); + } + + public Object getBean(String name) throws BeansException { + return delegate.getBean(name); + } + + public Object resolveDependency(DependencyDescriptor descriptor, + String beanName) throws BeansException { + return delegate.resolveDependency(descriptor, beanName); + } + + public T getBean(String name, Class requiredType) + throws BeansException { + return delegate.getBean(name, requiredType); + } + + @SuppressWarnings("rawtypes") + public Object createBean(Class beanClass, int autowireMode, + boolean dependencyCheck) throws BeansException { + return delegate.createBean(beanClass, autowireMode, dependencyCheck); + } + + public T getBean(Class requiredType) throws BeansException { + return delegate.getBean(requiredType); + } + + @SuppressWarnings("rawtypes") + public Object autowire(Class beanClass, int autowireMode, + boolean dependencyCheck) throws BeansException { + return delegate.autowire(beanClass, autowireMode, dependencyCheck); + } + + public Object getBean(String name, Object... args) throws BeansException { + return delegate.getBean(name, args); + } + + public void autowireBeanProperties(Object existingBean, int autowireMode, + boolean dependencyCheck) throws BeansException { + delegate.autowireBeanProperties(existingBean, autowireMode, + dependencyCheck); + } + + public boolean containsBean(String name) { + return delegate.containsBean(name); + } + + public boolean isSingleton(String name) + throws NoSuchBeanDefinitionException { + return delegate.isSingleton(name); + } + + public void applyBeanPropertyValues(Object existingBean, String beanName) + throws BeansException { + delegate.applyBeanPropertyValues(existingBean, beanName); + } + + public boolean isPrototype(String name) + throws NoSuchBeanDefinitionException { + return delegate.isPrototype(name); + } + + @SuppressWarnings("rawtypes") + public boolean isTypeMatch(String name, Class targetType) + throws NoSuchBeanDefinitionException { + return delegate.isTypeMatch(name, targetType); + } + + public Object initializeBean(Object existingBean, String beanName) + throws BeansException { + return delegate.initializeBean(existingBean, beanName); + } + + public Class getType(String name) throws NoSuchBeanDefinitionException { + return delegate.getType(name); + } + + public Object applyBeanPostProcessorsBeforeInitialization( + Object existingBean, String beanName) throws BeansException { + return delegate.applyBeanPostProcessorsBeforeInitialization( + existingBean, beanName); + } + + public String[] getAliases(String name) { + return delegate.getAliases(name); + } + + public Object applyBeanPostProcessorsAfterInitialization( + Object existingBean, String beanName) throws BeansException { + return delegate.applyBeanPostProcessorsAfterInitialization( + existingBean, beanName); + } + + public Object resolveDependency(DependencyDescriptor descriptor, + String beanName, Set autowiredBeanNames, + TypeConverter typeConverter) throws BeansException { + return delegate.resolveDependency(descriptor, beanName, + autowiredBeanNames, typeConverter); + } + + +} diff --git a/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/LoggingConfig.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/LoggingConfig.java new file mode 100644 index 0000000000..fc3a28dc1b --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/LoggingConfig.java @@ -0,0 +1,88 @@ +/** + * 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.checkArgument; +import static org.jclouds.logging.LoggingModules.firstOrJDKLoggingModule; + +import java.util.Set; + +import javax.annotation.PostConstruct; + +import org.jclouds.logging.Logger; +import org.jclouds.logging.Logger.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor; + +/** + * Spring config that sets up {@link CommonAnnotationBeanPostProcessor} support + * for injecting loggers. + * + * @author Andrew Phillips + */ +abstract class LoggingConfig implements BeanFactoryAware { + protected static final LoggerFactory LOGGER_FACTORY = firstOrJDKLoggingModule().createLoggerFactory(); + + private static final Logger LOGGER = LOGGER_FACTORY.getLogger(LoggingConfig.class.getName()); + + private AutowireCapableBeanFactory beanFactory; + + @PostConstruct + public void initLoggerSupport() { + CommonAnnotationBeanPostProcessor resourceProcessor = + (CommonAnnotationBeanPostProcessor) beanFactory.getBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME); + resourceProcessor.setResourceFactory(new LoggerResourceBeanFactory(beanFactory)); + } + + private static class LoggerResourceBeanFactory extends DelegatingAutowireCapableBeanFactory { + + LoggerResourceBeanFactory(AutowireCapableBeanFactory delegate) { + super(delegate); + } + + @Override + public Object resolveDependency(DependencyDescriptor descriptor, + String beanName, Set autowiredBeanNames, + TypeConverter typeConverter) throws BeansException { + Object bean; + if (descriptor.getDependencyType().equals(Logger.class)) { + Class requestingType = getType(beanName); + LOGGER.trace("About to create logger for bean '%s' of type '%s'", + beanName, requestingType); + bean = LOGGER_FACTORY.getLogger(requestingType.getName()); + LOGGER.trace("Successfully created logger."); + return bean; + } + return super.resolveDependency(descriptor, beanName, autowiredBeanNames, typeConverter); + } + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + checkArgument(beanFactory instanceof AutowireCapableBeanFactory, "expected an instance of '%s' but was '%s'", + AutowireCapableBeanFactory.class, beanFactory.getClass()); + this.beanFactory = (AutowireCapableBeanFactory) beanFactory; + } +} \ No newline at end of file diff --git a/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/SpringServletConfig.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/SpringServletConfig.java new file mode 100644 index 0000000000..25bdbd80e6 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/SpringServletConfig.java @@ -0,0 +1,236 @@ +/** + * 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.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.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; + +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.blobstore.BlobStoreContextFactory; +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.controller.AddTweetsController; +import org.jclouds.demo.tweetstore.controller.EnqueueStoresController; +import org.jclouds.demo.tweetstore.controller.StoreTweetsController; +import org.jclouds.demo.tweetstore.functions.ServiceToStoredTweetStatuses; +import org.jclouds.logging.Logger; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.context.ServletConfigAware; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.handler.SimpleServletHandlerAdapter; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; + +import twitter4j.Twitter; +import twitter4j.TwitterFactory; +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.common.io.Closeables; +import com.google.inject.Module; + +/** + * Creates servlets (using resources from the {@link SpringAppConfig}) and mappings. + * + * @author Andrew Phillips + * @see SpringAppConfig + */ +@Configuration +public class SpringServletConfig extends LoggingConfig implements ServletConfigAware { + public static final String PROPERTY_BLOBSTORE_CONTEXTS = "blobstore.contexts"; + + private static final Logger LOGGER = LOGGER_FACTORY.getLogger(SpringServletConfig.class.getName()); + + private ServletConfig servletConfig; + + private Map providerTypeToBlobStoreMap; + private Twitter twitterClient; + private String container; + private TaskQueue queue; + private String baseUrl; + + @PostConstruct + public void initialize() throws IOException { + BlobStoreContextFactory blobStoreContextFactory = new BlobStoreContextFactory(); + + Properties props = loadJCloudsProperties(); + LOGGER.trace("About to initialize members."); + + Set modules = ImmutableSet.of(); + // shared across all blobstores and used to retrieve tweets + try { + twitter4j.conf.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, blobStoreContextFactory.createContext(hint, modules, props)); + } + + // get a queue for submitting store tweet requests and the application's base URL + PlatformServices platform = PlatformServices.get(servletConfig.getServletContext()); + queue = platform.getTaskQueue("twitter"); + baseUrl = platform.getBaseUrl(); + + LOGGER.trace("Members initialized. Twitter: '%s', container: '%s', provider types: '%s'", twitterClient, + container, providerTypeToBlobStoreMap.keySet()); + } + + 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; + } + + private Properties loadJCloudsProperties() { + LOGGER.trace("About to read properties from '%s'", "/WEB-INF/jclouds.properties"); + Properties props = new Properties(); + InputStream input = servletConfig.getServletContext().getResourceAsStream("/WEB-INF/jclouds.properties"); + try { + props.load(input); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + Closeables.closeQuietly(input); + } + LOGGER.trace("Properties successfully read."); + return props; + } + + @Bean + public StoreTweetsController storeTweetsController() { + StoreTweetsController controller = new StoreTweetsController(providerTypeToBlobStoreMap, container, twitterClient); + injectServletConfig(controller); + return controller; + } + + @Bean + public AddTweetsController addTweetsController() { + AddTweetsController controller = new AddTweetsController(providerTypeToBlobStoreMap, + serviceToStoredTweetStatuses()); + injectServletConfig(controller); + return controller; + } + + @Bean + public EnqueueStoresController enqueueStoresController() { + return new EnqueueStoresController(providerTypeToBlobStoreMap, queue, baseUrl); + } + + private void injectServletConfig(Servlet servlet) { + LOGGER.trace("About to inject servlet config '%s'", servletConfig); + try { + servlet.init(checkNotNull(servletConfig)); + } catch (ServletException exception) { + throw new BeanCreationException("Unable to instantiate " + servlet, exception); + } + LOGGER.trace("Successfully injected servlet config."); + } + + @Bean + ServiceToStoredTweetStatuses serviceToStoredTweetStatuses() { + return new ServiceToStoredTweetStatuses(providerTypeToBlobStoreMap, container); + } + + @Bean + public HandlerMapping handlerMapping() { + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + Map urlMap = Maps.newHashMapWithExpectedSize(2); + urlMap.put("/store/*", storeTweetsController()); + urlMap.put("/tweets/*", addTweetsController()); + urlMap.put("/stores/*", enqueueStoresController()); + mapping.setUrlMap(urlMap); + /* + * "/store", "/tweets" and "/stores" are part of the servlet mapping and thus + * stripped by the mapping if using default settings. + */ + mapping.setAlwaysUseFullPath(true); + return mapping; + } + + @Bean + public HandlerAdapter servletHandlerAdapter() { + return new SimpleServletHandlerAdapter(); + } + + @PreDestroy + public void destroy() throws Exception { + LOGGER.trace("About to close contexts."); + for (BlobStoreContext context : providerTypeToBlobStoreMap.values()) { + context.close(); + } + LOGGER.trace("Contexts closed."); + LOGGER.trace("About to purge request queue."); + queue.destroy(); + LOGGER.trace("Request queue purged."); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.web.context.ServletConfigAware#setServletConfig(javax.servlet.ServletConfig + * ) + */ + @Override + public void setServletConfig(ServletConfig servletConfig) { + this.servletConfig = servletConfig; + } +} \ No newline at end of file diff --git a/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollector.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollector.java new file mode 100644 index 0000000000..ce3943376e --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/util/PropertiesLoader.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/config/util/PropertiesLoader.java new file mode 100644 index 0000000000..dafa5c311b --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/controller/AddTweetsController.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/controller/AddTweetsController.java new file mode 100644 index 0000000000..7254941332 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/controller/AddTweetsController.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 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.cloudfoundry.runtime.env.ApplicationInstanceInfo; +import org.cloudfoundry.runtime.env.CloudEnvironment; +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 + public 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())); + // TODO: remove me! + ApplicationInstanceInfo instanceInfo = new CloudEnvironment().getInstanceInfo(); + request.setAttribute("instanceInfo", instanceInfo); + } + + public List apply(Set in) { + List statuses = Lists.newArrayList(); + for (Iterable list : Iterables.transform(in, + blobStoreContextToContainerResult)) { + Iterables.addAll(statuses, list); + } + return statuses; + } +} \ No newline at end of file diff --git a/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresController.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresController.java new file mode 100644 index 0000000000..592eaaa8bd --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/controller/StoreTweetsController.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/controller/StoreTweetsController.java new file mode 100644 index 0000000000..725ba128f6 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/domain/StoredTweetStatus.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/domain/StoredTweetStatus.java new file mode 100644 index 0000000000..42ad65df01 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatus.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatus.java new file mode 100644 index 0000000000..2a6ea0a69c --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatuses.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatuses.java new file mode 100644 index 0000000000..0807c7bb46 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatuses.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 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.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 = context.getProviderSpecificContext().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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/reference/TweetStoreConstants.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/reference/TweetStoreConstants.java new file mode 100644 index 0000000000..42ec480ae2 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/reference/TwitterConstants.java b/demos/tweetstore/cf-tweetstore-spring/src/main/java/org/jclouds/demo/tweetstore/reference/TwitterConstants.java new file mode 100644 index 0000000000..dc8b97915f --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/platform/.gitignore b/demos/tweetstore/cf-tweetstore-spring/src/main/platform/.gitignore new file mode 100644 index 0000000000..843dfe79c0 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/resources/jobs.xml b/demos/tweetstore/cf-tweetstore-spring/src/main/resources/jobs.xml new file mode 100644 index 0000000000..b740fdd52f --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/resources/quartz.properties b/demos/tweetstore/cf-tweetstore-spring/src/main/resources/quartz.properties new file mode 100644 index 0000000000..12a0fcfe91 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/main/webapp/WEB-INF/web.xml b/demos/tweetstore/cf-tweetstore-spring/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..4ca14fb517 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,64 @@ + + + + jclouds-tweetstore + + + org.jclouds.demo.paas.config.PlatformServicesInitializer + + + + + dispatcher + org.springframework.web.servlet.DispatcherServlet + + + contextClass + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + contextConfigLocation + org.jclouds.demo.tweetstore.config.SpringServletConfig + + + + + + dispatcher + /store/* + + + dispatcher + /tweets/* + + + dispatcher + /stores/* + + + + index.jsp + + \ No newline at end of file diff --git a/demos/tweetstore/cf-tweetstore-spring/src/main/webapp/images/cloudfoundry-logo.png b/demos/tweetstore/cf-tweetstore-spring/src/main/webapp/images/cloudfoundry-logo.png new file mode 100644 index 0000000000..2df231c26c Binary files /dev/null and b/demos/tweetstore/cf-tweetstore-spring/src/main/webapp/images/cloudfoundry-logo.png differ diff --git a/demos/tweetstore/cf-tweetstore-spring/src/main/webapp/index.jsp b/demos/tweetstore/cf-tweetstore-spring/src/main/webapp/index.jsp new file mode 100644 index 0000000000..6365c49c09 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/main/webapp/index.jsp @@ -0,0 +1,30 @@ +<%-- + + 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.

+

Powered by Cloud Foundry

+ + diff --git a/demos/tweetstore/cf-tweetstore-spring/src/main/webapp/tweets.jsp b/demos/tweetstore/cf-tweetstore-spring/src/main/webapp/tweets.jsp new file mode 100644 index 0000000000..b066bd9167 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/main/webapp/tweets.jsp @@ -0,0 +1,108 @@ +<%-- + + 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

+ + + + + + + +
+
+ + + + + + + + +
+
Powered by Cloud Foundry
+ + diff --git a/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollectorTest.java b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/config/util/CredentialsCollectorTest.java new file mode 100644 index 0000000000..031bb199fc --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/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/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/controller/AddTweetsControllerTest.java b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/controller/AddTweetsControllerTest.java new file mode 100644 index 0000000000..c8e1241e4a --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/controller/AddTweetsControllerTest.java @@ -0,0 +1,77 @@ +/** + * 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.blobstore.BlobStoreContext; +import org.jclouds.blobstore.BlobStoreContextFactory; +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(); + for (String name : new String[] { "1", "2" }) { + BlobStoreContext context = new BlobStoreContextFactory().createContext("transient", + "dummy", "dummy"); + 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/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresControllerTest.java b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresControllerTest.java new file mode 100644 index 0000000000..ff3b651175 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/controller/EnqueueStoresControllerTest.java @@ -0,0 +1,83 @@ +/** + * 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.blobstore.BlobStoreContext; +import org.jclouds.blobstore.BlobStoreContextFactory; +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() { + Map contexts = ImmutableMap.of( + "test1", new BlobStoreContextFactory().createContext("transient", "dummy", "dummy"), + "test2", new BlobStoreContextFactory().createContext("transient", "dummy", "dummy")); + 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/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/controller/StoreTweetsControllerTest.java b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/controller/StoreTweetsControllerTest.java new file mode 100644 index 0000000000..0e82a13f25 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/controller/StoreTweetsControllerTest.java @@ -0,0 +1,118 @@ +/** + * 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.expect; +import static org.easymock.classextension.EasyMock.createMock; +import static org.easymock.classextension.EasyMock.replay; +import static org.easymock.classextension.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.blobstore.BlobMap; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.blobstore.BlobStoreContextFactory; +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 { + Map contexts = ImmutableMap. of("test1", + new BlobStoreContextFactory().createContext("transient", "dummy", "dummy"), "test2", + new BlobStoreContextFactory().createContext("transient", "dummy", "dummy")); + 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/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatusTest.java b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatusTest.java new file mode 100644 index 0000000000..58153eb3ed --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/functions/KeyToStoredTweetStatusTest.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.functions; + +import static org.testng.Assert.assertEquals; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +import org.jclouds.blobstore.BlobMap; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.blobstore.BlobStoreContextFactory; +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 = new BlobStoreContextFactory().createContext("transient", "dummy", + "dummy"); + 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/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatusesTest.java b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatusesTest.java new file mode 100644 index 0000000000..51df008762 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/functions/ServiceToStoredTweetStatusesTest.java @@ -0,0 +1,74 @@ +/** + * 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.blobstore.BlobStoreContext; +import org.jclouds.blobstore.BlobStoreContextFactory; +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(); + for (String name : new String[] { "1", "2" }) { + BlobStoreContext context = new BlobStoreContextFactory().createContext("transient", + "dummy", "dummy"); + 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/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/integration/CloudFoundryServer.java b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/integration/CloudFoundryServer.java new file mode 100644 index 0000000000..6ecebbdfab --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/integration/CloudFoundryServer.java @@ -0,0 +1,106 @@ +/** + * 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.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.io.Closeables.closeQuietly; +import static java.lang.String.format; +import static org.jclouds.demo.tweetstore.integration.util.Zips.zipDir; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.cloudfoundry.client.lib.CloudApplication.AppState; +import org.cloudfoundry.client.lib.CloudFoundryClient; + +/** + * Basic "server facade" functionality to deploy a WAR to Cloud Foundry. + * + * @author Andrew Phillips + */ +public class CloudFoundryServer { + private static final String CLOUD_FOUNDRY_APPLICATION_URL_SUFFIX = ".cloudfoundry.com"; + + protected CloudFoundryClient client; + protected String appName; + + public void writePropertiesAndStartServer(final String address, final String warfile, + String target, String username, String password, Properties props) throws IOException, InterruptedException, ExecutionException { + String propsfile = String.format("%1$s/WEB-INF/jclouds.properties", warfile); + System.err.println("file: " + propsfile); + storeProperties(propsfile, props); + assert new File(propsfile).exists(); + + client = new CloudFoundryClient(username, password, target); + client.login(); + appName = getAppName(address); + deploy(warfile); + client.logout(); + TimeUnit.SECONDS.sleep(10); + } + + private void deploy(String explodedWar) throws IOException { + File war = zipDir(explodedWar, format("%s-cloudfoundry.war", explodedWar)); + client.uploadApplication(appName, war); + + // adapted from https://github.com/cloudfoundry/vcap-java-client/blob/master/cloudfoundry-maven-plugin/src/main/java/org/cloudfoundry/maven/Update.java + AppState appState = client.getApplication(appName).getState(); + switch (appState) { + case STOPPED: + client.startApplication(appName); + break; + case STARTED: + client.restartApplication(appName); + break; + default: + throw new IllegalStateException(format("Unexpected application state '%s'", appState)); + } + } + + private static void storeProperties(String filename, Properties props) + throws IOException { + FileOutputStream targetFile = new FileOutputStream(filename); + try { + props.store(targetFile, "test"); + } finally { + closeQuietly(targetFile); + } + } + + private static String getAppName(String applicationUrl) { + checkArgument(applicationUrl.endsWith(CLOUD_FOUNDRY_APPLICATION_URL_SUFFIX), + "Application URL '%s' does not end in '%s'", applicationUrl, + CLOUD_FOUNDRY_APPLICATION_URL_SUFFIX); + + return applicationUrl.substring(0, + applicationUrl.length() - CLOUD_FOUNDRY_APPLICATION_URL_SUFFIX.length()); + } + + public void stop() throws Exception { + checkState(client != null, "'stop' called before 'writePropertiesAndStartServer'"); + client.login(); + client.stopApplication(appName); + client.logout(); + } +} \ No newline at end of file diff --git a/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/integration/TweetStoreLiveTest.java b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/integration/TweetStoreLiveTest.java new file mode 100644 index 0000000000..18bdfe395f --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/integration/TweetStoreLiveTest.java @@ -0,0 +1,233 @@ +/** + * 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.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.blobstore.BlobStoreContext; +import org.jclouds.blobstore.BlobStoreContextFactory; +import org.jclouds.demo.tweetstore.config.SpringServletConfig; +import org.jclouds.demo.tweetstore.controller.StoreTweetsController; +import org.jclouds.logging.log4j.config.Log4JLoggingModule; +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.Joiner; +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 Google App Engine for Java Development environment and deploys an application which + * tests accesses twitter and blobstores. + * + * @author Adrian Cole + */ +@Test(groups = "live", singleThreaded = true) +public class TweetStoreLiveTest { + + CloudFoundryServer server; + private URL url; + private Map contexts; + private String container; + + private static final Iterable blobstores = + Splitter.on(',').split(System.getProperty(PROPERTY_TWEETSTORE_BLOBSTORES, + "cloudfiles-us,aws-s3,azureblob")); + 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); + props.setProperty(SpringServletConfig.PROPERTY_BLOBSTORE_CONTEXTS, Joiner.on(',').join(blobstores)); + + // put all identity/credential pairs into the client + addCredentialsForBlobStores(props); + + // example of an ad-hoc client configuration + addConfigurationForTwitter(props); + + final BlobStoreContextFactory factory = new BlobStoreContextFactory(); + // for testing, capture logs. + final Set wiring = ImmutableSet. of(new Log4JLoggingModule()); + this.contexts = Maps.newConcurrentMap(); + + for (String provider : blobstores) { + contexts.put(provider, factory.createContext(provider, wiring, props)); + } + + 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()) { + if (context.getBlobStore().containerExists(container)) { + System.err.printf("deleting container %s at %s%n", container, context.getProviderSpecificContext() + .getEndpoint()); + context.getBlobStore().deleteContainer(container); + deleted = true; + } + } + 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.getProviderSpecificContext() + .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 + @Parameters({ "warfile", "cloudfoundry.address", "cloudfoundry.port", "cloudfoundry.target", "cloudfoundry.username", "cloudfoundry.password" }) + public void startDevAppServer(final String warfile, final String address, final String port, + String target, String username, String password) throws Exception { + url = new URL(String.format("http://%s:%s", address, port)); + server = new CloudFoundryServer(); + server.writePropertiesAndStartServer(address, warfile, target, username, password, 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("X-Platform-Originator", "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.getProviderSpecificContext().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/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/integration/util/Zips.java b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/integration/util/Zips.java new file mode 100644 index 0000000000..571c9083a3 --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/test/java/org/jclouds/demo/tweetstore/integration/util/Zips.java @@ -0,0 +1,36 @@ +/** + * 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.io.File; +import java.io.IOException; + +import org.codehaus.plexus.archiver.zip.ZipArchiver; + +public class Zips { + + public static File zipDir(String dirToZip, String zipFile) throws IOException { + ZipArchiver archiver = new ZipArchiver(); + archiver.addDirectory(new File(dirToZip)); + File zip = new File(zipFile); + archiver.setDestFile(zip); + archiver.createArchive(); + return zip; + } +} \ No newline at end of file diff --git a/demos/tweetstore/cf-tweetstore-spring/src/test/resources/log4j.xml b/demos/tweetstore/cf-tweetstore-spring/src/test/resources/log4j.xml new file mode 100644 index 0000000000..2e5d01fb9e --- /dev/null +++ b/demos/tweetstore/cf-tweetstore-spring/src/test/resources/log4j.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +