diff --git a/appmap-java-maven-plugin/README.md b/appmap-java-maven-plugin/README.md
new file mode 100644
index 00000000..b9f3b8b6
--- /dev/null
+++ b/appmap-java-maven-plugin/README.md
@@ -0,0 +1,67 @@
+AppLand AppMap Maven Plugin for Java
+--------------------------------
+
+
+- [Building](#building)
+- [Agent Configuration](#agent-configuration)
+- [Maven Plugin Configuration](#maven-plugin-config)
+ - [Plugin Goals](#plugin-goals)
+ - [Plugin configuration options](#plugin-configuration)
+ - [Example](#example)
+
+
+# Building
+Artifacts will be written to `target/` use `appmap-java-plugin-[VERSION].jar`. as your maven plugin.
+```bash
+$ mvn clean install
+```
+
+# Agent Configuration
+When you run your program, the agent reads configuration settings from `appmap.yml` by default.
+
+Please read configuration options from [AppMap Java Agent README.md](../README.md)
+
+# Maven Plugin Configuration
+
+## Plugin goals
+prepare-agent : adds appmap.jar to JVM execution as javaagent
+
+## Plugin configuration options
+outputDirectory (default: ./target/appmap/)
+configFile (default: ./appmap.yml)
+debug (enabled|disabled, default: disabled)
+eventValueSize (integer, default 1024)
+skip(Boolean, default false)
+
+## Example plugin config in a standard POM.xml file
+```xml
+
+
+ com.appland
+ appmap-maven-plugin
+ ${appmap-java.version}
+
+
+ appmap.yml
+ enabled
+ 1024
+ false
+
+
+
+
+ prepare-agent
+
+
+
+
+```
+
+
+# Running
+To run the java agent with correct plugin configuration you only need to build your project as usual without skipping
+the test goal.
+
+```bash
+$ mvn clean install
+```
diff --git a/appmap-java-maven-plugin/pom.xml b/appmap-java-maven-plugin/pom.xml
new file mode 100644
index 00000000..751e5921
--- /dev/null
+++ b/appmap-java-maven-plugin/pom.xml
@@ -0,0 +1,70 @@
+
+ 4.0.0
+ com.appland
+ appmap-maven-plugin
+ maven-plugin
+ 0.5.0-SNAPSHOT
+ Appland Java Recorder Maven Plugin
+ This maven plugin helps you automatically generate an Appland AppMap using the Java Recorder.
+
+ https://github.com/applandinc/appmap-java/tree/master/java-maven-plugin
+
+
+ UTF-8
+ UTF-8
+
+
+
+
+ com.appland
+ appmap-agent
+ 0.5.0
+
+
+
+ org.apache.maven
+ maven-plugin-api
+ 3.6.1
+
+
+ org.apache.maven.plugin-tools
+ maven-plugin-annotations
+ 3.6.0
+
+
+ org.apache.maven
+ maven-core
+ 3.5.4
+
+
+
+ junit
+ junit
+ 4.13.1
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-plugin-plugin
+ 3.6.0
+
+
+ true
+
+
+
+ mojo-descriptor
+
+ descriptor
+
+
+
+
+
+
+
diff --git a/appmap-java-maven-plugin/src/main/java/com/appland/appmap/AppMapAgentMojo.java b/appmap-java-maven-plugin/src/main/java/com/appland/appmap/AppMapAgentMojo.java
new file mode 100644
index 00000000..dfeeeb30
--- /dev/null
+++ b/appmap-java-maven-plugin/src/main/java/com/appland/appmap/AppMapAgentMojo.java
@@ -0,0 +1,123 @@
+package com.appland.appmap;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+
+import java.io.File;
+import java.util.*;
+
+import static java.lang.String.format;
+
+public abstract class AppMapAgentMojo extends AbstractMojo {
+
+ static final String APPMAP_AGENT_ARTIFACT_NAME = "com.appland:appmap-agent";
+ static final String SUREFIRE_ARG_LINE = "argLine";
+
+ @Parameter(property = "skip")
+ protected boolean skip = false;
+
+ @Parameter(property = "project.outputDirectory")
+ protected File outputDirectory = new File("tmp");
+
+ @Parameter(property = "project.configFile")
+ protected File configFile = new File("appmap.yml");
+
+ @Parameter(property = "project.debug")
+ protected String debug = "disabled";
+
+ @Parameter(property = "project.eventValueSize")
+ protected Integer eventValueSize = 1024;
+
+ @Parameter(property = "plugin.artifactMap")
+ protected Map pluginArtifactMap;
+
+ @Parameter(property = "project")
+ private MavenProject project;
+
+ public abstract void execute() throws MojoExecutionException;
+
+ protected void skipMojo() {
+ }
+
+ protected void loadAppMapJavaAgent() {
+ final String newValue = buildArguments();
+ setProjectArgLineProperty(newValue);
+ getLog().info(SUREFIRE_ARG_LINE
+ + " set to " + StringEscapeUtils.unescapeJava(newValue));
+ }
+
+ /**
+ * This method builds the needed parameter to run the Agent, if previous configuration is found is also attached in
+ * the SUREFIRE_ARG_LINE, if previous version of the AppMap agent is found is removed and replaced with the version
+ * of this maven plugin
+ *
+ * @return formatted and escaped arguments to run on command line
+ */
+ private String buildArguments() {
+ List args = new ArrayList();
+ final String oldConfig = getCurrentArgLinePropertyValue();
+ if (oldConfig != null) {
+ final List oldArgs = Arrays.asList(oldConfig.split(" "));
+ removeOldAppMapAgentFromCommandLine(oldArgs);
+ args.addAll(oldArgs);
+ }
+ addMvnAppMapCommandLineArgsFirst(args);
+ StringBuilder builder = new StringBuilder();
+ for ( String arg : args) {
+ builder.append(arg).append(" ");
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Generate required quotes JVM argument based on current configuration and
+ * prepends it to the given argument command line. If a agent with the same
+ * JAR file is already specified this parameter is removed from the existing
+ * command line, does the same for xbootclasspath command.
+ */
+ private void removeOldAppMapAgentFromCommandLine(List oldArgs) {
+ final String plainAgent = format("-javaagent:%s", getAppMapAgentJar());
+ final String xbootClasspath = format("-Xbootclasspath/a:%s", getAppMapAgentJar());
+ for (final Iterator i = oldArgs.iterator(); i.hasNext(); ) {
+ final String oldCommand = i.next();
+ if (oldCommand.startsWith(plainAgent) || oldCommand.startsWith(xbootClasspath)) {
+ i.remove();
+ }
+ }
+ }
+
+ private void addMvnAppMapCommandLineArgsFirst(List args) {
+ args.add(StringEscapeUtils.escapeJava(
+ format("-Xbootclasspath/a:%s", getAppMapAgentJar(), this)
+ ));
+ args.add(StringEscapeUtils.escapeJava(
+ format("-javaagent:%s=%s", getAppMapAgentJar(), this)
+ ));
+
+ args.add(0, "-Dappmap.debug=" + StringEscapeUtils.escapeJava(debug));
+ args.add(0, "-Dappmap.output.directory=" + StringEscapeUtils.escapeJava(format("%s", outputDirectory)));
+ args.add(0, "-Dappmap.config.file=" + StringEscapeUtils.escapeJava(format("%s", configFile)));
+ args.add(0, "-Dappmap.event.valueSize=" + eventValueSize);
+ }
+
+
+ private Object setProjectArgLineProperty(String newValue) {
+ return project.getProperties().setProperty(SUREFIRE_ARG_LINE, newValue);
+ }
+
+ private String getCurrentArgLinePropertyValue() {
+ return project.getProperties().getProperty(SUREFIRE_ARG_LINE);
+ }
+
+ protected File getAppMapAgentJar() {
+ return pluginArtifactMap.get(APPMAP_AGENT_ARTIFACT_NAME).getFile();
+ }
+
+ public MavenProject getProject() {
+ return project;
+ }
+}
diff --git a/appmap-java-maven-plugin/src/main/java/com/appland/appmap/LoadJavaAppMapAgentMojo.java b/appmap-java-maven-plugin/src/main/java/com/appland/appmap/LoadJavaAppMapAgentMojo.java
new file mode 100644
index 00000000..2a6e8728
--- /dev/null
+++ b/appmap-java-maven-plugin/src/main/java/com/appland/appmap/LoadJavaAppMapAgentMojo.java
@@ -0,0 +1,32 @@
+package com.appland.appmap;
+
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+
+/**
+ * Goal that adds appmap.jar to JVM execution as javaagent,
+ * right before the test execution begins.
+ */
+@Mojo(name = "prepare-agent", defaultPhase = LifecyclePhase.TEST_COMPILE)
+public class LoadJavaAppMapAgentMojo extends AppMapAgentMojo {
+
+ @Override
+ public void execute()
+ throws MojoExecutionException {
+ try {
+ if (skip) {
+ getLog().info("Skipping AppLand AppMap execution because property skip is set.");
+ skipMojo();
+ return;
+ } else {
+ getLog().info("Initializing AppLand AppMap Java Recorder." );
+ loadAppMapJavaAgent();
+ }
+ } catch (Exception e) {
+ getLog().error("Error initializing AppLand AppMap Java Recorder");
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/appmap-java-maven-plugin/src/test/java/com/appland/appmap/LoadJavaMapAgentMojoTest.java b/appmap-java-maven-plugin/src/test/java/com/appland/appmap/LoadJavaMapAgentMojoTest.java
new file mode 100644
index 00000000..e8ca9d78
--- /dev/null
+++ b/appmap-java-maven-plugin/src/test/java/com/appland/appmap/LoadJavaMapAgentMojoTest.java
@@ -0,0 +1,51 @@
+package com.appland.appmap;
+
+import static org.junit.Assert.assertEquals;
+
+import com.appland.appmap.output.v1.Event;
+
+import com.appland.appmap.record.IRecordingSession;
+import com.appland.appmap.record.Recorder;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+public class LoadJavaMapAgentMojoTest {
+
+ @Before
+ public void before() throws Exception {
+ final IRecordingSession.Metadata metadata =
+ new IRecordingSession.Metadata();
+
+ Recorder.getInstance().start(metadata);
+ }
+
+ @Test
+ public void testAllEventsWritten() {
+ final Recorder recorder = Recorder.getInstance();
+ final Long threadId = Thread.currentThread().getId();
+ final Event[] events = new Event[] {
+ new Event(),
+ new Event(),
+ new Event(),
+ };
+
+ for (int i = 0; i < events.length; i++) {
+ final Event event = events[i];
+ event
+ .setDefinedClass("SomeClass")
+ .setMethodId("SomeMethod")
+ .setStatic(false)
+ .setLineNumber(315)
+ .setThreadId(threadId);
+
+ recorder.add(event);
+ assertEquals(event, recorder.getLastEvent());
+ }
+
+ final String appmapJson = recorder.stop();
+ final String expectedJson = "\"thread_id\":" + threadId.toString();
+ final int numMatches = StringUtils.countMatches(appmapJson, expectedJson);
+ assertEquals(numMatches, events.length);
+ }
+}