Heroku deploy local jar to Maven Dependencies

Sometime a jar is not available via Maven Central Repository and you need to load a jar from your local filesystem. Copy the jar file to a lib directory in your project.

Execute the following command via command line

mvn deploy:deploy-file -Durl=file:salesforce/lib/ 
-Dfile=salesforce/lib/emp-connector-0.0.1-SNAPSHOT-phat.jar 
-DgroupId=com.salesforce.conduit 
-Dpackaging=jar 
-Dversion=0.0.1-SNAPSHOT 
-DartifactId=emp-connector

Add the following to maven pom.xml

 <repositories>
    <repository>
        <id>project.local</id>
        <name>project</name>
        <url>file:${project.basedir}/lib</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
 </repositories>
 <dependencies>
    <dependency>
        <groupId>com.salesforce.conduit</groupId>
        <artifactId>emp-connector</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

Run Spring Boot Scheduler as Heroku Worker

1. Setup Application
2. Setup Scheduler
3. Add Spring Boot dependency
4. Add Procfile
5. Scale Heroku worker

1. Setup Application

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * Created by tmichels on 9/20/15.
 */

@SpringBootApplication
@EnableScheduling
public class Application {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(Application.class);
    }
}

2. Setup Scheduler

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Set;

/**
 * Created by tmichels on 9/18/15.
 */

@Component
public class Scheduler {

    private static final Logger logger = LoggerFactory.getLogger(Scheduler.class);

    @Scheduled(cron = "0 0 13 * * *")
    public void buyHighPercentageReturnNotes(){
       System.out.println("Scheduler is running");
    }
}

3. Add Spring Boot dependency

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.6.RELEASE</version>
    </parent>
  <dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter</artifactId>
      </dependency>
  </dependencies>

4. Add Procfile

worker: java $JAVA_OPTS -jar target/*.jar

5. Scale Heroku worker

heroku ps:scale worker=1

Metrics Dashboard using STOMP WebSocket

http://cloudsole-metrics.herokuapp.com
https://github.com/thysmichels/cloudsole-metrics

1. Basic WebAppInitializer
2. WebConfiguration with EnableScheduling
3. WebSocketConfiguration with EnableWebSocketMessageBroker
4. Random Number Generator with ApplicationListener
5. Sock.js Javascript

1. Basic WebAppInitializer

package  com.thysmichels.websockets.configuration;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class WebAppInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(final ServletContext context) throws ServletException {

        final AnnotationConfigWebApplicationContext root = new AnnotationConfigWebApplicationContext();

        root.scan("com.thysmichels.websockets");

        context.addListener(new ContextLoaderListener(root));

        final ServletRegistration.Dynamic appServlet = context.addServlet("appServlet",new DispatcherServlet(new GenericWebApplicationContext()));
        appServlet.setAsyncSupported(true);
        appServlet.setLoadOnStartup(1);
        appServlet.addMapping("/*");
    }
}

2. WebConfiguration with EnableScheduling

package  com.thysmichels.websockets.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@EnableWebMvc
@EnableScheduling
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void configureDefaultServletHandling(
        final DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

3. WebSocketConfiguration with EnableWebSocketMessageBroker

package  com.thysmichels.websockets.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
    }

    @Override
    public void registerStompEndpoints(final StompEndpointRegistry registry) {
        registry.addEndpoint("/metrics").withSockJS();
    }

    @Override
    public void configureClientInboundChannel(final ChannelRegistration registration) {}

    @Override
    public void configureClientOutboundChannel(final ChannelRegistration registration) { }

}

4. Random Number Generator with ApplicationListener

package  com.thysmichels.websockets.utils;

import java.util.Random;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.core.MessageSendingOperations;
import org.springframework.messaging.simp.broker.BrokerAvailabilityEvent;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class RandomDataGenerator implements ApplicationListener<BrokerAvailabilityEvent> {

    private final MessageSendingOperations<String> messagingTemplate;

    @Autowired
    public RandomDataGenerator(final MessageSendingOperations<String> messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    @Override
    public void onApplicationEvent(final BrokerAvailabilityEvent event) {
    }

    @Scheduled(fixedDelay = 1000)
    public void sendDataUpdates() {
        this.messagingTemplate.convertAndSend("/data", new Random().nextInt(100));
    }
}

5. Sock.js Javascript

/**
 * Created by tmichels on 8/19/14.
 */

//var stompClient = null;
var updateOpts = {'minVal':'0','maxVal':'100','newVal':'1'};

var randomData;
var socket = new SockJS('/metrics');
var client = Stomp.over(socket);

client.connect('admin', 'password', function(frame) {

    client.subscribe("/data", function(message) {
        var point = [ (new Date()).getTime(), parseInt(message.body) ];
        gaugeUpdate('cf-gauge-1', {'minVal':'0','maxVal':'100','newVal':parseInt(message.body)})
        $('#spark-1').each(function(){

            customSparkOptions = {};
            customSparkOptions.minSpotColor = true;
            var sparkOptions = cf_defaultSparkOpts;
            var sparkOptions = $.extend({}, cf_defaultSparkOpts, customSparkOptions);

            data.push(parseInt(message.body));
            createSparkline($(this), data, sparkOptions);
        });

        $('#metric-1 .metric').html(message.body);
        $('#metric-1 .large').html(message.body);
        $('#metric-2 .metric').html(message.body);
        $('#metric-2 .large').html(message.body);
        var element = $(this).data('update');
        cf_rSVPs[$('#svp-1').attr('id')].chart.update(parseInt(message.body));
        $('#svp-1 .chart').data('percent', parseInt(message.body));
        $('#svp-1 .metric').html(message.body);
       // $('#cf-svmc-sparkline .metric').html(message.body);

        $('#cf-rag-1').each(function(){
            // Dummy data for RAG
            ragData = [60,30,parseInt(message.body)];
            ragLabels = ['Success','Bounce','Abandoned'];
            ragOpts = {postfix:'%'}

            cf_rRags[$(this).prop('id')] = new RagChart($(this).prop('id'), ragData, ragLabels, ragOpts);
        });

        $('#cf-pie-1').each(function(){

            var pdata = [
                {
                    value : parseInt(message.body),
                    color : pieSegColors[3],
                    label: 'Success'
                },
                {
                    value : parseInt(message.body)+50,
                    color : pieSegColors[2],
                    label: 'Bounce'
                },
                {
                    value: parseInt(message.body)+100,
                    color: pieSegColors[1],
                    label: 'Abandoned'
                }
            ]

            var $container = $(this);
            var pId = $container.prop('id');

            // Store chart information
            cf_rPs[pId] = {};
            cf_rPs[pId].data = pdata;


             // Set options per chart
             customOptions = {};
             customOptions.animation = false;
             cf_rPs[pId].options = customOptions;

            // Create chart
            createPieChart($container);
        });

        $('#cf-funnel-1').each(function(){
            funData = [parseInt(message.body)+3000,parseInt(message.body)+1500,parseInt(message.body)+500,parseInt(message.body)+250,parseInt(message.body)];
            funLabels = ['Visits','Cart','Checkout','Purchase','Refund'];
            funOptions = {barOpacity:true, layout:'left'};

            cf_rFunnels[$(this).prop('id')] = new FunnelChart($(this).prop('id'), funData, funLabels, funOptions);
        });

    });
});


Heroku OAuth 2.0 Scribe Spring MVC Example

ApplicationContext.xml defines herokuServiceConfig and herokuServiceProvider Bean

  <bean id="herokuServiceConfig" class="com.example.oauth.OAuthServiceConfig">
		<constructor-arg value="xxx" />
		<constructor-arg value="xxx"/>
		<constructor-arg value="https://www.example.com/oauth/heroku"/>
		<constructor-arg value="com.example.oauth.HerokuOauthApi"/>
	</bean>
	
	<bean id="herokuServiceProvider" class="com.example.oauth.OAuthServiceProvider">
		<constructor-arg name="config" ref="herokuServiceConfig" />
	</bean>

Spring MVC Heroku OAuth Controller

package com.example.oauth.controller;

import static org.springframework.web.context.request.RequestAttributes.SCOPE_SESSION;

import java.util.Map;

import javax.servlet.http.HttpSession;

import org.scribe.model.Token;
import org.scribe.model.Verifier;
import org.scribe.oauth.OAuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.WebRequest;

import com.example.oauth.OAuthServiceProvider;

/**
 * @author tmichels
 */

@Controller
@RequestMapping("/oauth/heroku")
public class HerokuController {
	
	@Autowired
	@Qualifier("herokuServiceProvider")
	private OAuthServiceProvider herokuServiceProvider;
	
	private static final Token EMPTY_TOKEN = null;
	
	@RequestMapping(value="/login-heroku", method=RequestMethod.GET)
	 public String loginToHeroku(Map<String, Object> map, WebRequest request) {
			OAuthService service = herokuServiceProvider.getService();
			String authUrl = service.getAuthorizationUrl(EMPTY_TOKEN);
			System.out.println("RequestToken: " + authUrl );
			return "redirect:" + authUrl;
    }
	
	@RequestMapping(value={""}, method = RequestMethod.GET)
	public String callback(@RequestParam(value="oauth_token", required=false) String oauthToken,
			@RequestParam(value="code", required=false) String oauthVerifier, WebRequest request, Map<String, Object> map) {

		OAuthService service = herokuServiceProvider.getService();

		// getting access token
		Verifier verifier = new Verifier(oauthVerifier);
		Token accessToken = service.getAccessToken(EMPTY_TOKEN, verifier);

		// store access token as a session attribute
		request.setAttribute("oauthAccessToken", accessToken, SCOPE_SESSION);

		ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
		HttpSession session = attr.getRequest().getSession(false); //create a new session
		session.setAttribute("accessToken",accessToken);
		
		return "settings";
	}
		
}

HerokuOauthApi extends DefaultApi20

package com.example.oauth;

import org.scribe.builder.api.DefaultApi20;
import org.scribe.extractors.AccessTokenExtractor;
import org.scribe.extractors.JsonTokenExtractor;
import org.scribe.model.OAuthConfig;
import org.scribe.model.Verb;

public class HerokuOauthApi extends DefaultApi20{

	private static final String ACCESSTOKEN = "https://id.heroku.com/oauth/token";

	@Override
	public String getAccessTokenEndpoint() {
		return ACCESSTOKEN;
	}

	@Override
	public String getAuthorizationUrl(OAuthConfig config) {
		return String.format("https://id.heroku.com/oauth/authorize?client_id=%s&response_type=code&scope=global", config.getApiKey());
	}
	
	 @Override
	 public Verb getAccessTokenVerb(){
	       return Verb.POST;
	 }
	 
	 @Override
	 public AccessTokenExtractor getAccessTokenExtractor() {
	    return new JsonTokenExtractor();
	 }
}

New-Relic Add-on for Java Application on Heroku Setup

Add New Relic Agent Dependency

 <dependency>
  	<groupId>com.newrelic.agent.java</groupId>
  	<artifactId>newrelic-agent</artifactId>
  	<version>3.5.1</version>
  	<scope>provided</scope>
</dependency>

pom.xml build tag

  <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
                <version>2.3.2</version>
            </plugin>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.2</version>
            </plugin>
            
            <plugin>
				<groupId>com.mysema.maven</groupId>
				<artifactId>apt-maven-plugin</artifactId>
				<version>1.0.8</version>
				<dependencies>
					<dependency>
						<groupId>com.mysema.querydsl</groupId>
						<artifactId>querydsl-apt</artifactId>
						<version>2.9.0</version>
					</dependency>
				</dependencies>
				<executions>
					<execution>
						<phase>generate-sources</phase>
						<goals>
							<goal>process</goal>
						</goals>
						<configuration>
							<outputDirectory>target/generated-sources</outputDirectory>
							<processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
						</configuration>
					</execution>
				</executions>
			</plugin>
            
            
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.6</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                           <goal>copy</goal>
                        </goals>
                         
                        <configuration>
                            <artifactItems>
                                <artifactItem>
                                    <groupId>com.github.jsimone</groupId>
                                    <artifactId>webapp-runner</artifactId>
                                    <version>7.0.22.3</version>
                                    <destFileName>webapp-runner.jar</destFileName>
                                </artifactItem>
                            </artifactItems>
                        </configuration>
                    </execution>
                   <execution>
         				 <id>copy-new-relic</id>
          					<phase>package</phase>
         				<goals>
          					 <goal>copy-dependencies</goal>
         				 </goals>
         				 <configuration>
           					 <includeGroupIds>com.newrelic.agent.java</includeGroupIds>
           					 <includeArtifactIds>newrelic-agent</includeArtifactIds>
           					 <stripVersion>true</stripVersion>
          					</configuration>
        			</execution>
                </executions>
            </plugin>
        </plugins>
        <pluginManagement>
	<plugins>
    <!-- Ignore/Execute plugin execution -->
    <plugin>
        <groupId>org.eclipse.m2e</groupId>
        <artifactId>lifecycle-mapping</artifactId>
        <version>1.0.0</version>
        <configuration>
            <lifecycleMappingMetadata>
                <pluginExecutions>
                    <!-- copy-dependency plugin -->
                    <pluginExecution>
                        <pluginExecutionFilter>
                            <groupId>org.apache.maven.plugins</groupId>
                            <artifactId>maven-dependency-plugin</artifactId>
                            <versionRange>[1.0.0,)</versionRange>
                            <goals>
                                <goal>copy-dependencies</goal>
                            </goals>
                        </pluginExecutionFilter>
                        <action>
                            <ignore />
                        </action>
                    </pluginExecution>
                </pluginExecutions>
            </lifecycleMappingMetadata>
        </configuration>
    </plugin>
   </plugins></pluginManagement>
        
    </build>
    <name>CloudSole Developer</name>
    <description>CloudSole Developer</description>
    <organization>
    	<name>Thys Michels</name>
    	<url>http://www.thysmichels.com</url>
    </organization>

Create newrelic.yml and save to project route (same level as pom.xml)

.....
license_key: 'OURLICENCEKEYHERE'
....
app_name: CloudSole
....

Add the following flag to your JAVA_OPTS config var on Heroku:

heroku config:set JAVA_OPTS='-Xmx384m -Xss512k -XX:+UseCompressedOops -javaagent:target/dependency/newrelic-agent.jar' NEW_RELIC_APP_NAME="CloudSole"

Setup Memcached on Heroku

Add memcachedcloud to your project:
heroku addons:add memcachedcloud

1. Setup Memcached Configuration
2. Setup Memcached Service

1. Setup Memcached Configuration

package com.example.cache;

import java.io.IOException;

import net.spy.memcached.AddrUtil;
import net.spy.memcached.ConnectionFactoryBuilder;
import net.spy.memcached.MemcachedClient;
import net.spy.memcached.auth.AuthDescriptor;
import net.spy.memcached.auth.PlainCallbackHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CacheConfiguration {
	
	@Bean
	public MemcachedClient memcachedCloudConfiguration() {
		try {
		    AuthDescriptor ad = new AuthDescriptor(new String[] { "PLAIN" },
		        new PlainCallbackHandler(System.getenv("MEMCACHEDCLOUD_USERNAME"), System.getenv("MEMCACHEDCLOUD_PASSWORD")));

		    MemcachedClient mc = new MemcachedClient(
		              new ConnectionFactoryBuilder()
		                  .setProtocol(ConnectionFactoryBuilder.Protocol.BINARY)
		                  .setAuthDescriptor(ad).build(),
		          AddrUtil.getAddresses(System.getenv("MEMCACHEDCLOUD_SERVERS")));
		    return mc;
		} catch (IOException ex) {
		    // the Memcached client could not be initialized.
		}
		return null;
	}
}

2. Setup Memcached Service

package com.example.cache;

import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import net.spy.memcached.MemcachedClient;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Service;

import com.example.model.Customer;
import com.example.service.CustomerService;

@Service
@Import({CacheConfiguration.class})
public class CacheCustomerService implements CustomerService{

	@Autowired MemcachedClient memCachedClient;
	
	@Override
	public Customer getCustomerById(BigInteger id) {
		return (Customer) memCachedClient.get(String.valueOf(id));
	}

	@Override
	public void createCustomer(String fn, String ln) {
		memCachedClient.set("customerKey", 3600, new Customer(fn, ln));
	}

	@Override
	public void deleteCustomer(BigInteger id) {
		memCachedClient.delete(String.valueOf(id));
	}

	@Override
	public void createCustomer(Customer newCustomer) {
		memCachedClient.set("customerKey", 3600, newCustomer);
	}	
}

Setup Quartz on Heroku

There are two ways to use Quartz on Heroku:
1. Setup your own Quartz on Heroku
2. Use Heroku Quartz Addon

1. Setup your own Quartz on Heroku

Create Postgres db:
heroku addons:add heroku-postgresql:dev

Get Postgres properties:
heroku config

Use these properties to specify your:

  • org.quartz.dataSource.dataSource.URL
  • org.quartz.dataSource.dataSource.user
  • org.quartz.dataSource.dataSource.password

Your quartz.properties file

org.quartz.scheduler.instanceName = BatchScheduler
org.quartz.threadPool.threadCount = 3
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = dataSource
org.quartz.dataSource.dataSource.driver = org.postgresql.Driver
org.quartz.dataSource.dataSource.URL = jdbc:postgresql://ec2-23-21-133-106.compute-1.amazonaws.com:5432/dsqpq2m83g9jc
org.quartz.dataSource.dataSource.user = YOURUSERNAME
org.quartz.dataSource.dataSource.password = YOURPASSWORD
org.quartz.dataSource.dataSource.maxConnections = 1 

Create your Quartz tables in Postgres:
heroku pg:psql

Copy your Postgres query in the console:
Quartz tables for Quartz 2.1.6

Make sure you are using the same version of tables as specified in your pom.xml in this case my pom looks like:

<dependency>
      <groupId>org.quartz-scheduler</groupId>
      <artifactId>quartz</artifactId>
      <version>2.1.6</version>
</dependency>

2. Use Heroku Quartz Addon

Add Heroku scheduler to your project:
heroku addons:add scheduler:standard

Add the Quartz class Heroku must call in your Procfile:
scheduler: java $JAVA_OPTS -cp target/classes:target/dependency/* com.example.service.SchedulerService

Example of SchedulerService:

package com.example.service;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.repeatSecondlyForever;
import static org.quartz.TriggerBuilder.newTrigger;

public class SchedulerService {
	
    final static Logger logger = LoggerFactory.getLogger(SchedulerService.class);
    public static void main(String[] args) throws Exception {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        scheduler.start();
        JobDetail jobDetail = newJob(DatabaseJob.class).build();
        Trigger trigger = newTrigger()
                .startNow()
                .withSchedule(repeatSecondlyForever(5))
                .build();
        scheduler.scheduleJob(jobDetail, trigger);
    }

    public static class DatabaseJob implements Job {
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            try {
              //Add your job that needs to execute here
            }
            catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        }
    }
}