diff --git a/build.gradle b/build.gradle index 2acd0cc068..ac12331bd7 100644 --- a/build.gradle +++ b/build.gradle @@ -108,6 +108,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2" compile 'com.squareup.okhttp3:okhttp:3.3.1' + compile 'co.paralleluniverse:capsule:1.0.3' // Unit testing helpers. testCompile 'junit:junit:4.12' @@ -196,46 +197,49 @@ applicationDistribution.into("bin") { fileMode = 0755 } -task createCapsule(type: FatCapsule, dependsOn: 'quasarScan') { +task buildCordaJAR(type: FatCapsule, dependsOn: 'quasarScan') { applicationClass 'com.r3corda.node.MainKt' + archiveName 'corda.jar' + applicationSource = files(project.tasks.findByName('jar'), 'build/classes/main/CordaCaplet.class') capsuleManifest { appClassPath = ["jolokia-agent-war-${project.ext.jolokia_version}.war"] systemProperties['log4j.configuration'] = 'log4j2.xml' javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"] minJavaVersion = '1.8.0' + caplets = ['CordaCaplet'] } } -task createStandalone(dependsOn: 'createCapsule') << { +task installTemplateNodes(dependsOn: 'buildCordaJAR') << { copy { - from createCapsule.outputs.getFiles() + from buildCordaJAR.outputs.getFiles() from 'config/dev/nameservernode.conf' - into "${buildDir}/standalone/nameserver" + into "${buildDir}/nodes/nameserver" rename 'nameservernode.conf', 'node.conf' } copy { - from createCapsule.outputs.getFiles() + from buildCordaJAR.outputs.getFiles() from 'config/dev/generalnodea.conf' - into "${buildDir}/standalone/nodea" + into "${buildDir}/nodes/nodea" rename 'generalnodea.conf', 'node.conf' } copy { - from createCapsule.outputs.getFiles() + from buildCordaJAR.outputs.getFiles() from 'config/dev/generalnodeb.conf' - into "${buildDir}/standalone/nodeb" + into "${buildDir}/nodes/nodeb" rename 'generalnodeb.conf', 'node.conf' } - delete("${buildDir}/standalone/runstandalone") - def jarName = createCapsule.outputs.getFiles().getSingleFile().getName() + delete("${buildDir}/nodes/runnodes") + def jarName = buildCordaJAR.outputs.getFiles().getSingleFile().getName() copy { - from "buildSrc/scripts/runstandalone" + from "buildSrc/scripts/runnodes" filter { String line -> line.replace("JAR_NAME", jarName) } filter(org.apache.tools.ant.filters.FixCrLfFilter.class, eol: org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance("lf")) - into "${buildDir}/standalone" + into "${buildDir}/nodes" } } \ No newline at end of file diff --git a/buildSrc/scripts/runstandalone b/buildSrc/scripts/runnodes similarity index 71% rename from buildSrc/scripts/runstandalone rename to buildSrc/scripts/runnodes index dad232ec07..a6bec7cef8 100644 --- a/buildSrc/scripts/runstandalone +++ b/buildSrc/scripts/runnodes @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# Creates three nodes. A network map and notary node and two regular nodes that can be extended with cordapps. + set -euo pipefail trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT export CAPSULE_CACHE_DIR=cache diff --git a/docs/source/creating-a-cordapp.rst b/docs/source/creating-a-cordapp.rst new file mode 100644 index 0000000000..3831eb9c07 --- /dev/null +++ b/docs/source/creating-a-cordapp.rst @@ -0,0 +1,65 @@ +Creating a Cordapp +================== + +A Cordapp is an application that runs on the Corda platform using the platform APIs and plugin system. They are self +contained in separate JARs from the node server JAR that are created and distributed. + +App Plugins +----------- + +.. note:: Currently apps are only supported for JVM languages. + +To create an app plugin you must you must extend from `CordaPluginRegistry`_. The JavaDoc contains +specific details of the implementation, but you can extend the server in the following ways: + +1. Required protocols: Specify which protocols will be whitelisted for use in your web APIs. +2. Service plugins: Register your :ref:`services`. +3. Web APIs: You may register your own endpoints under /api/ of the built-in web server. +4. Static web endpoints: You may register your own static serving directories for serving web content. + +Services +-------- + +.. _services: + +Services are classes which are constructed after the node has started. It is provided a `ServiceHubInternal`_ which +allows a richer API than the `ServiceHub`_ exposed to contracts. It enables adding protocols, registering +message handlers and more. The service does not run in a separate thread, so the only entry point to the service is during +construction, where message handlers should be registered and threads started. + + +Starting Nodes +-------------- + +To use an app you must also have a node server. To create a node server run the gradle installTemplateNodes task. + +This will output the node JAR to ``build/libs/corda.jar`` and several sample/standard +node setups to ``build/nodes``. For now you can use the ``build/nodes/nodea`` configuration as a template. + +Each node server must have a ``node.conf`` file in the same directory as the node JAR file. After first +execution of the node server there will be many other configuration and persistence files created in this directory. + +.. note:: Outside of development environments do not store your node directories in the build folder. + +Installing Apps +------------------ + +Once you have created your app JAR you can install it to a node by adding it to ``/plugins/``. In this +case the ``node_dir`` is the location where your node server's JAR and configuration file is. + +.. note:: If the directory does not exist you can create it manually. + +Starting your Node +------------------ + +Now you have a node server with your app installed, you can run it by navigating to ```` and running + + java -jar corda.jar + +The plugin should automatically be registered and the configuration file used. + +.. warning:: If your working directory is not ```` your plugins and configuration will not be used. + +.. _CordaPluginRegistry: api/com.r3corda.core.node/-corda-plugin-registry/index.html +.. _ServiceHubInternal: api/com.r3corda.node.services.api/-service-hub-internal/index.html +.. _ServiceHub: api/com.r3corda.node.services.api/-service-hub/index.html \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 46da547db0..e1fbac846b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -31,6 +31,7 @@ Read on to learn: transaction-data-types consensus messaging + creating-a-cordapp running-the-demos node-administration diff --git a/src/main/java/CordaCaplet.java b/src/main/java/CordaCaplet.java new file mode 100644 index 0000000000..9f734bc968 --- /dev/null +++ b/src/main/java/CordaCaplet.java @@ -0,0 +1,54 @@ +// Due to Capsule being in the default package, which cannot be imported, this caplet +// must also be in the default package. When using Kotlin there are a whole host of exceptions +// trying to construct this from Capsule, so it is written in Java. + +import org.apache.commons.io.FilenameUtils; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +public class CordaCaplet extends Capsule { + + protected CordaCaplet(Capsule pred) { + super(pred); + } + + /** + * Overriding the Caplet classpath generation via the intended interface in Capsule. + */ + @Override + @SuppressWarnings("unchecked") + protected T attribute(Map.Entry attr) { + // Equality is used here because Capsule never instantiates these attributes but instead reuses the ones + // defined as public static final fields on the Capsule class, therefore referential equality is safe. + if(ATTR_APP_CLASS_PATH == attr) { + T cp = super.attribute(attr); + List classpath = (List) cp; + return (T) augmentClasspath(classpath); + } + return super.attribute(attr); + } + + // TODO: Make directory configurable via the capsule manifest. + // TODO: Add working directory variable to capsules string replacement variables. + private List augmentClasspath(List classpath) { + File dir = new File("plugins"); + if(!dir.exists()) { + dir.mkdir(); + } + + File[] files = dir.listFiles(); + for (File file : files) { + if (file.isFile() && isJAR(file)) { + classpath.add(file.toPath().toAbsolutePath()); + } + } + return classpath; + } + + private Boolean isJAR(File file) { + return file.getName().toLowerCase().endsWith(".jar"); + } +}