();
+
+ /*
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * 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.
+
+
+
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
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+