Skip to content

Model And Plugin Design

Dominic Chambers edited this page Nov 15, 2013 · 4 revisions

Introduction

The basic idea of the new BladeRunnerJS model is to provide a single encapsulated store of all BladeRunnerJS domain knowledge. This will be provided as a navigable, self-discoverable object model, in much the same vein as the Document Object Model present in all browsers. This is not only useful internally, allowing us to discard lots of duplicated code containing this domain knowledge and to get rid of lots of latent bugs due to the current lack of encapsulation, but will also be critical externally so that developers can create BladeRunnerJS plugins that perform useful work.

Design Goals

  • The API must make any future refactoring of our directory structures easy to do.
  • There should be very little cost required to create new model nodes, with most of the code being shared between all nodes.
  • Exploratory model navigation must be easy for the reader to comprehend.
  • We should use logical rather than physical navigation whenever possible.
  • The reader should be able to differentiate physical navigation (which should be avoided) from logical navigation.
  • Location and ancestor discovery for bad paths should fail slow when this is just going to be used to do downwards navigation.
  • Upwards discovery can use the cache, whereas downwards discovery must refresh the cache.
  • Exploratory model navigation can add to the cache if directory existence caching or automatic path creation are used.

These design goals were reverse engineered from this set of observed directory model access use cases:

  • Root Access (null -> root model-node)
  • Existent Ancestor Discovery (directory -> typed model-node)
  • Existent Child Discovery (model-node -> child model-nodes)
  • Logical Exploratory Model Navigation (model-node -> model-node)
  • Physical Model Navigation (model-node -> model-node)
  • Directory Access (model-node -> directory)
  • Directory Existence Checking
  • Automatic Path Creation

Some Examples

  • brjs.locate(fileWithinABlade, Blade.class).seedResources();
  • brjs.app("fxtrader").bladeset("fx").blade("ticket").dirExists();
  • brjs.app("fxtrader").bladeset("fx").blade("ticket").create();
  • brjs.app("fxtrader").bladeset("fx").blade("ticket").runTests();

Example Directory Structure

The directory structure that the final model will need to be able to support is as follows:

.
|-- apps
|   |-- a1
|   |   |-- a1-aspect
|   |   |   |-- resources
|   |   |   |   |-- aliases.xml
|   |   |   |   |-- html
|   |   |   |   |-- i18n
|   |   |   |   `-- xml
|   |   |   |-- src
|   |   |   |-- tests
|   |   |   |   |-- test-type1
|   |   |   |   |   |-- tech1
|   |   |   |   |   |   |-- resources
|   |   |   |   |   |   |   |-- aliases.xml
|   |   |   |   |   |   |   |-- html
|   |   |   |   |   |   |   |-- i18n
|   |   |   |   |   |   |   `-- xml
|   |   |   |   |   |   |-- src-test
|   |   |   |   |   |   `-- tests
|   |   |   |   |   `-- tech2
|   |   |   |   `-- test-type2
|   |   |   |-- themes
|   |   |   |   |-- t1
|   |   |   |   `-- t2
|   |   |   `-- unbundled-resources
|   |   |       `-- empty.txt
|   |   |-- a2-aspect
|   |   |-- bladeset-bs1
|   |   |   |-- blades
|   |   |   |   |-- b1
|   |   |   |   |   |-- resources
|   |   |   |   |   |   |-- aliasDefinitions.xml
|   |   |   |   |   |   |-- html
|   |   |   |   |   |   |-- i18n
|   |   |   |   |   |   `-- xml
|   |   |   |   |   |-- src
|   |   |   |   |   |-- tests
|   |   |   |   |   |   |-- test-type1
|   |   |   |   |   |   |   |-- tech1
|   |   |   |   |   |   |   |   |-- resources
|   |   |   |   |   |   |   |   |   |-- aliases.xml
|   |   |   |   |   |   |   |   |   |-- html
|   |   |   |   |   |   |   |   |   |-- i18n
|   |   |   |   |   |   |   |   |   `-- xml
|   |   |   |   |   |   |   |   |-- src-test
|   |   |   |   |   |   |   |   `-- tests
|   |   |   |   |   |   |   `-- tech2
|   |   |   |   |   |   `-- test-type2
|   |   |   |   |   |-- themes
|   |   |   |   |   |   |-- t1
|   |   |   |   |   |   `-- t2
|   |   |   |   |   `-- workbench
|   |   |   |   |       |-- resources
|   |   |   |   |       |   |-- aliases.xml
|   |   |   |   |       |   |-- html
|   |   |   |   |       |   |-- i18n
|   |   |   |   |       |   |-- style
|   |   |   |   |       |   `-- xml
|   |   |   |   |       |-- src
|   |   |   |   |       `-- test
|   |   |   |   `-- b2
|   |   |   |-- resources
|   |   |   |   |-- aliasDefinitions.xml
|   |   |   |   |-- html
|   |   |   |   |-- i18n
|   |   |   |   `-- xml
|   |   |   |-- src
|   |   |   |   `-- empty.txt
|   |   |   |-- tests
|   |   |   |   |-- test-type1
|   |   |   |   |   |-- tech1
|   |   |   |   |   |   |-- resources
|   |   |   |   |   |   |   |-- aliases.xml
|   |   |   |   |   |   |   |-- html
|   |   |   |   |   |   |   |-- i18n
|   |   |   |   |   |   |   `-- xml
|   |   |   |   |   |   |-- src-test
|   |   |   |   |   |   `-- tests
|   |   |   |   |   `-- tech2
|   |   |   |   `-- test-type2
|   |   |   `-- themes
|   |   |       |-- t1
|   |   |       `-- t2
|   |   `-- bladeset-bs2
|   `-- a2
|-- conf
|   |-- bladerunner.conf
|   |-- java
|   |-- test-runner.conf
|   `-- users.properties
|-- integration-test-conf
|-- js-patches
|-- performance-test-conf
|-- sdk
|   |-- bladerunner
|   |-- bladerunnerAscii.txt
|   |-- bladerunner.cmd
|   |-- docs
|   |   |-- jsdoc
|   |   |   `-- empty.txt
|   |   `-- release-notes
|   |-- libs
|   |   |-- java
|   |   |   |-- application
|   |   |   |-- system
|   |   |   `-- testRunner
|   |   `-- javascript
|   |       |-- caplin
|   |       |   |-- resources
|   |       |   |   |-- aliasDefinitions.xml
|   |       |   |   |-- html
|   |       |   |   |-- i18n
|   |       |   |   `-- xml
|   |       |   `-- src
|   |       `-- thirdparty
|   |           |-- l1
|   |           |   `-- library.manifest
|   |           `-- l2
|   |-- log
|   |-- loginRealm.conf
|   |-- system-applications
|   |-- templates
|   |   |-- app-template
|   |   |-- aspect-template
|   |   |-- bladeset-template
|   |   |-- blade-template
|   |   `-- cutlass-template
|   `-- version.txt

BladeRunner Directory Model UML

A model that hopefully supports this directory structure, while abstracting away any relationship to the underlying directory structure wherever possible, is as follows:

Part I

Part II

Model Support Classes UML

These classes are used by the model, but are not directly part of it:

Plugin Interfaces UML

Plugins are discovered components that implement one of a number of pre-defined interfaces. The designs for these interfaces have been reached using the following new use cases:

  • LessCss Support Including Sourcemaps (BundlerPlugin)
  • CommonJS Support (FileTransformPlugin)
  • EcmaScript 6 Support (FileTransformPlugin)
  • TypeScript Support Including Sourcemaps (BundlerPlugin)
  • CoffeeScript Support Including Sourcemaps (FileTransformPlugin or BundlerPlugin)
  • Individually Served JavaScript Files In Development (BundlerPlugin)
  • Closure Compiler In Production Including Sourcemaps (BundlerPlugin)
  • UglifyJs2 In Production Including Sourcemaps (BundlerPlugin)
  • Ability To Include All Bundles Of A Given Type (LogicalTagPlugin)
  • Js-Test-Driver Support (TestPlugin)
  • WebDriver Support (TestPlugin)
  • Karma Support (TestPlugin)
  • Multi-Core Testing Support (TestPlugin)

Plus of course, it was heavily affected by the desire to retain all the advantages of the Caplin version of BladeRunner that we already had.

High Level Exceptions UML

High level exception classes are used to allow the controller code to determine how error messages should be presented to the user and/or what should happen next:

Model Engine Utility UML

The following utility classes are made available so that rich directory models (like the one above), with legacy support and backwards compatibility support can quickly be created and modified:

Code To Handle A Bundle Request

BRJS.handleLogicalRequest():


package com.caplin.brjs.model;

public class BRJS extends AbstractBRJSRootNode {
	// snip...

	private LogicalRequestHandler requestHandler = new LogicalRequestHandler();

	// snip...

	public void handleLogicalRequest(BladerunnerUri requestUri, java.io.OutputStream os) throws BundlerPluginException, IOException {
		requestHandler.handle(requestUri, os);
	}

	// snip...
}

LogicalRequestHandler:


package com.caplin.brjs.model.utility;

public class LogicalRequestHandler {
	private Map bundlers = new HashMap<>();

	public void handle(BladerunnerUri requestUri, OutputStream os) throws BundlerPluginException, IOException {
		File baseDir = new File(BRJS.root.dir(), requestUri.scopePath);
		BundlableNode bundlableNode = BRJS.root.locateFirstBundlableAncestorNode(baseDir);
		BundlerPlugin bundler = bundlers.get(getResourceBundlerName(requestUri));
		ParsedRequest request = (ParsedRequest) bundler.getRequestParser().parse(requestUri.logicalPath);

		bundler.handleRequest(request, bundlableNode, os);
	}

	private String getResourceBundlerName(BladerunnerUri requestUri) {
		return requestUri.logicalPath.substring(requestUri.logicalPath.lastIndexOf('.') + 1);
	}
}

Js Bundler Plugins

JsLogicalTagPlugin:


package com.caplin.brjs.core.plugin.resourcebundler.js;

public class JsLogicalTagPlugin implements LogicalTagPlugin {
	@Override
	public String getTagName() {
		return "js.bundle";
	}

	@Override
	public void writeDevTag(Map tagAttributes, BundlableNode bundlableNode, String locale, Writer writer) throws IOException {
		for (BundlerPlugin bundler : bundlableNode.parentApp().bundlerPlugins("text/javascript")) {
			writeRequests(writer, bundler.getDevBundlerRequests(bundlableNode, locale));
		}
	}

	@Override
	public void writeProdTag(Map tagAttributes, BundlableNode bundlableNode, String locale, Writer writer) throws IOException {
		for (BundlerPlugin bundler : bundlableNode.parentApp().bundlerPlugins("text/javascript")) {
			writeRequests(writer, bundler.getProdBundlerRequests(bundlableNode, locale));
		}
	}

	private void writeRequests(Writer writer, List requests) throws IOException {
		for (String request : requests) {
			writer.write("<script type='text/javascript' src='" + request + "'></script>\n");
	}
}

}

JsBundlerPlugin:


package com.caplin.brjs.core.plugin.bundler.js;

public class JsBundlerPlugin implements BundlerPlugin {
	private final RequestParser requestParser;

	{
		RequestParserBuilder requestParserBuilder = new RequestParserBuilder();
		requestParserBuilder.accepts("js/-bundle.js").as("bundle-request")
			.and("js/src/.js").as("source-file-request");
		requestParser = requestParserBuilder.build();
	}

	@Override
	public String getMimeType() {
		return "text/javascript";
	}

	@Override
	public String getBundlerName() {
		return "js";
	}

	@Override
	public RequestParser getRequestParser() {
		return requestParser;
	}

	@Override
	public List getDevBundlerRequests(BundlableNode bundlableNode, String locale) {
		List requests = new ArrayList<>();

		for(SourceFile sourceFile : bundlableNode.getBundleSet().getSourceFiles()) {
			requests.add(requestParser.createRequest("single-file", sourceFile.getClassName()));
		}

		return requests;
	}

	@Override
	public List getProdBundlerRequests(BundlableNode bundlableNode, String locale) {
		List requests = new ArrayList<>();
		requests.add(requestParser.createRequest("bundle", getBundlerName()));

		return requests;
	}

	@Override
	public void handleRequest(ParsedRequest request, BundlableNode bundlableNode, OutputStream os) throws BundlerPluginException, IOException {
		// TODO: ensure we use the correct character encoding
		Writer writer = new OutputStreamWriter(os);

		if(request.requestFormName.equals("source-file-request")) {
			SourceFile sourceFile = bundlableNode.getClassFile(request.requestProperties.get("sourceFile"));
			IOUtils.copy(sourceFile.getReader(), writer);
		}
		else if(request.requestFormName.equals("bundle-request")) {
			for(SourceFile sourceFile : bundlableNode.getBundleSet().getSourceFiles()) {
				writer.write("// " + sourceFile.getClassName() + "\n");
				IOUtils.copy(sourceFile.getReader(), writer);
				writer.write("\n\n");
			}
		}
	}
}

BladeRunner Core

  • Commands:
    • DependencyAnalyserCommandPlugin
    • CheckCommandPlugin
    • CopyBladesetCommandPlugin
    • CreateApplicationCommandPlugin
    • CreateAspectCommandPlugin
    • CreateBladeCommandPlugin
    • CreateBladesetCommandPlugin
    • ImportApplicationCommandPlugin
    • BladerunnerCommandPlugin
    • TestCommandPlugin
    • TestServerCommandPlugin
    • ExportCommandPlugin
    • ExportMotifCommandPlugin
    • JsDocCommandPlugin
    • PackageDepsCommandPlugin
    • J2eeifyCommandPlugin
  • Bundlers (all bundlers also implement TagHandlerPlugin and ServletPlugin):
    • CompositeJsBundlerPlugin
    • NodeJsBundlerPlugin
    • CaplinJsBundlerPlugin
    • CompositeCssBundlerPlugin
    • CssBundlerPlugin
    • XmlBundlerPlugin
    • HtmlBundlerPlugin
    • I18nBundlerPlugin
    • ThirdPartyResourceBundlerPlugin
  • Minifiers:
    • SimpleConcatPlugin
    • ClosurePlugin
  • Tests:
    • JsTestDriverTestPlugin
    • WebDriverTestPlugin

CaplinTrader Plugins

  • Commands:
    • ResetDbCommandPlugin
  • File Transforms:
    • JNDIFileTransformPlugin
  • Model Observers:
    • ResetDbModelObserverPlugin

Anticipated Plugins

Although we're not creating these now, we need to ensure our interfaces will be able to support these plugins so that we or other developers can create them at a later date:

  • Commands:
    • ExportToHerokuCommandPlugin (deploy apps to the cloud)
    • ImportFromGitCommandPlugin (import apps shared on git)
    • CopyThemeCommandPlugin (create a new theme based on an old one)
  • Tag Handlers:
    • ImageTagPlugin (allow images to be used outside of CSS pages)
  • Bundlers:
    • JsonBundlerPlugin (allow JSON config to be bundled)
    • TypeScriptBundlerPlugin
    • CoffeeScriptBundlerPlugin
    • LessCssBundlerPlugin
  • File Transforms:
    • JsHintFileTransformPlugin (add console.warning() lines for any JsHint feedback)
    • TryInProductionFilterPlugin (allow try/catch to be used in production, but not dev)
  • Minifiers:
    • UglifyJsPlugin (multi-level sourcemap support)
Clone this wiki locally