Author: jawi
Date: Wed Apr 18 07:46:56 2012
New Revision: 1327406
URL: http://svn.apache.org/viewvc?rev=1327406&view=rev
Log:
Added an article on changing the deployment strategy of Ace.
Added:
ace/site/trunk/content/dev-doc/design/ace-deployment-strategies.mdtext
(with props)
ace/site/trunk/content/dev-doc/design/deployment_strategy_classdiagram.svg
(with props)
ace/site/trunk/content/dev-doc/design/deployment_strategy_update_seq.svg
(with props)
Added: ace/site/trunk/content/dev-doc/design/ace-deployment-strategies.mdtext
URL:
http://svn.apache.org/viewvc/ace/site/trunk/content/dev-doc/design/ace-deployment-strategies.mdtext?rev=1327406&view=auto
==============================================================================
--- ace/site/trunk/content/dev-doc/design/ace-deployment-strategies.mdtext
(added)
+++ ace/site/trunk/content/dev-doc/design/ace-deployment-strategies.mdtext Wed
Apr 18 07:46:56 2012
@@ -0,0 +1,216 @@
+# Ace deployment strategies
+_implementing custom update policies_
+
+## Introduction
+
+This article describes how Ace deploys new deployment packages to targets and
how this strategy can be adapted to support more sophisticated scenario's.
+The remainder of this article assumes the reader has basic knowledge of the
principles behind Ace, and has sufficient programming skills. For this
article, the latest code of Ace (0.8.1-SNAPSHOT, rev.1326140) was used.
+
+## Provisioning
+
+Apache Ace is all about provisioning software artifacts to targets. Software
artifacts, or simply, artifacts, can be compiled code, configuration files, or
any other artifact needed for a software system to operate. Provisioned
artifacts are bundled in so-called "deployment packages", that is comprised of
the differences between the target's current artifacts and the to-be-deployed
artifacts. To distinguish between deployment packages, each one is given its
own version. The increment of versions is done automatically by Ace when
changes are committed to its repository.
+When a new deployment package is available, it is not automatically sent to
the target as the communication between target and management server (Ace) is
unidirectional and initiated by the target. A target has to periodically check
whether new deployment packages are available, and if so, do something with it.
+
+## Understanding Ace's deployment strategy
+
+Upon startup, the management agent, which represents a target, schedules
several tasks that periodically synchronize the local state with that of the
management server. Two of these tasks relate to the handling of deployment
packages: `DeploymentCheckTask` and `DeploymentUpdateTask`. Figure 1 shows the
relationship between the various components.
+
+
+Figure 1: deployment strategy class diagram.
+
+The `DeploymentCheckTask` is responsible for periodically checking the
management server for newer versions of deployment packages and if found emits
an event. The `DeploymentUpdateTask` also periodically checks the management
server, and if it finds a newer version, automatically downloads and installs
it.
+
+
+Figure 2: deployment strategy sequence diagram.
+
+Figure 2 shows a more detailed sequence diagram of the deployment update
strategy in Ace. The scheduler in the management agent calls the run()-method
of the DeploymentUpdateTask once every _N_ seconds. This method in its turn
makes several calls to the `DeploymentService`, which acts as a facade for
other services in Ace. The `DeploymentService` is queried for the highest local
and remote versions. If the remote version is newer than the local version, a
deployment package containing the changes between the local and the remote
version is installed. Note that this installation is done in a best
effort strategy: if it fails, all changes are rolled back to what they were.
As a consequence, if the installation fails one time, it probably will fail the
next time as well.Â
+
+### Tweaking the deployment strategy
+
+By default, the management agent allows the interval in which the update task
is run to be customized. To change this interval, the management agent should
be started with the extra system property "`-Dsyncinterval=1500`". The value of
this property is interpreted as the interval time in milliseconds. Note that
this property also changes the interval of other tasks as well, so setting it
to a lower interval could potentially raise lots of traffic to your management
server.
+
+Another thing you can customize is whether or not the update task should run
at all. This is done by starting the management agent with the system property
"`-Dorg.apache.ace.deployment.task=disabled`". While this option is not really
useful out-of-the-box (the target is not be able to obtain any version at all)
it opens up a possibility for providing custom deployment strategies.
+
+## Implementing a custom deployment strategy
+
+To implement our own deployment strategy, we need to create a bundle that
provides the management agent with our update task. We will start with an
update task that simply prints something to the console each time it is run and
expand it later on. As can be seen in figure 1, a task is anything that
implements the `Runnable` interface, so our update task needs to do as well.
Furthermore, we will use the DependencyManager component from Felix to provide
us with the right dependent services, the most important one being the
`DeploymentService`.
+The (stub) code for our update task looks like:
+
+ #!java
+ package net.luminis.ace.updatetask;
+
+ import org.apache.ace.deployment.service.DeploymentService;
+ import org.osgi.service.log.LogService;
+
+ public class MyUpdateTask implements Runnable {
+ // injected by DependencyManager
+ Â Â Â private DeploymentService m_service;
+ Â Â Â private LogService m_logService;
+
+    public void run() {
+ Â Â Â Â Â Â Â System.out.println("Hello from MyUpdateTask: " + new
java.util.Date());
+ Â Â Â }
+ }
+
+The bundle activator that registers our update task as service:
+
+ #!java
+ package net.luminis.ace.updatetask;
+
+ import java.util.Properties;
+
+ import org.apache.ace.deployment.service.DeploymentService;
+ import org.apache.ace.scheduler.constants.SchedulerConstants;
+ import org.apache.felix.dm.DependencyActivatorBase;
+ import org.apache.felix.dm.DependencyManager;
+ import org.osgi.framework.BundleContext;
+ import org.osgi.service.log.LogService;
+
+ public class Activator extends DependencyActivatorBase {
+ public void init(BundleContext context, DependencyManager manager)
throws Exception {
+ Properties props = new Properties();
+ props.put(SchedulerConstants.SCHEDULER_NAME_KEY,
MyUpdateTask.class.getName());
+ props.put(SchedulerConstants.SCHEDULER_DESCRIPTION_KEY, "My own
deployment update service.");
+ props.put(SchedulerConstants.SCHEDULER_RECIPE, "5000");
+
+ manager.add(createComponent()
+ .setInterface(Runnable.class.getName(), props)
+ .setImplementation(new MyUpdateTask())
+ .add(createServiceDependency()
+ .setService(DeploymentService.class)
+ .setRequired(true)
+ )
+ .add(createServiceDependency()
+ .setService(LogService.class)
+ .setRequired(false)
+ )
+ );
+ }
+
+ public void destroy(BundleContext context, DependencyManager manager)
throws Exception {
+ // Nothing
+ }
+ }
+
+The activator isn't that different from any other one, the only interesting
part is the service properties that we register along with our update task. The
three properties are used by the Ace's scheduler to determine that our service
is actually a scheduled task. With the "recipe" key, the scheduler is told what
interval (in milliseconds) is to be used to execute our task. In our case the
recipe is set to 5000, causing our task to be run once every five seconds.
+To complete the whole cycle, we need some build scripting. For this, we use
[BndTools](http://bndtools.org):
+
+ #!bnd
+ Bundle-Name: My Update Task
+ Bundle-Version: 0.0.1
+ Bundle-SymbolicName: net.luminis.ace.updatetask
+ Bundle-Activator: net.luminis.ace.updatetask.Activator
+ Private-Package: net.luminis.ace.updatetask
+ Import-Package: *
+ -buildpath: org.apache.ace.deployment.task.base;version=0.8,\
+ Â Â Â Â Â org.apache.ace.scheduler.api;version=0.8,\
+ Â Â Â Â Â org.apache.felix.dependencymanager;version=3.0,\
+ Â Â Â Â Â osgi.core;version=4.2.0,\
+ Â Â Â Â Â osgi.cmpn;version=4.2.0
+
+To let the management agent to find and use our custom update task bundle, we
need to add the JAR-file to its class path, and start the management agent with
the following options:
+
+ #!sh
+ [localhost:~/]$ java -Dorg.apache.ace.deployment.task=disabled \
+
 -cp org.apache.ace.launcher-0.8.1-SNAPSHOT.jar:net.luminis.ace.updatetask-1.0.0.jar
\
+ Â org.apache.ace.launcher.Main \
+ Â bundle=net.luminis.ace.updatetask.Activator \
+ Â identification=myTarget \
+ Â discovery=http://localhost:8080
+
+Note that besides adding our bundle to the class path, we also have added the
`bundle` argument to tell the management agent to include our bundle activator
in its startup.
+If everything went well, one of the first few lines printed out on the console
should be something like:
+
+ Adding additional bundle activator: net.luminis.ace.updatetask.Activator
+ Not starting activator org.apache.ace.deployment.task.
+ ...
+
+After startup, our update task should print a message stating its existence to
the console every five seconds.
+
+With the boiler plating in place, its time to make our update task a little
more interesting. We change the code of our update task to the following (for
brevity, only the run() method is shown):
+
+ #!java
+    public void run() {
+ Â Â Â Â Â Â Â try {
+ Â Â Â Â Â Â Â Â Â Â Â Version local = m_service.getHighestLocalVersion();
+ Â Â Â Â Â Â Â Â Â Â Â Version remote =
m_service.getHighestRemoteVersion();
+
+ Â Â Â Â Â Â Â Â Â Â Â if ((remote != null) && ((local == null) ||
(remote.compareTo(local) > 0))) {
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â // Ask user whether this update should
proceed...
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Update available!
Upgrade from " + local + " to " + remote + " [Y/N]?");
+
+ Â Â Â Â Â Â Â Â BufferedReader reader = new BufferedReader(new
InputStreamReader(System.in));
+ Â Â Â Â Â Â Â Â String result = reader.readLine();
+
+ Â Â Â Â Â Â Â Â Â if ("y".equalsIgnoreCase(result)) {
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â // Ask the deployer service to
install this update...
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â m_service.installVersion(remote,
local);
+
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â
m_logService.log(LogService.LOG_INFO, "Update installed!");
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â }
+ Â Â Â Â Â Â Â Â Â Â Â }
+ Â Â Â Â Â Â Â } catch (IOException e) {
+ Â Â Â Â Â Â Â Â Â Â Â m_logService.log(LogService.LOG_WARNING, "Update
failed!", e);
+ Â Â Â Â Â Â Â } catch (Exception e) {
+ Â Â Â Â Â Â Â Â Â Â Â m_logService.log(LogService.LOG_WARNING, "Update
failed!", e);
+ Â Â Â Â Â Â Â }
+
+
+This new implementation first asks the `DeploymentService` what the current
local (= version current running on management agent) and remote (= version
available in the management server) versions are. If the remote version is
newer/higher than the local version, than we ask the user for permission to
install the update. When given, the `DeploymentService` is requested to upgrade
from the local version to the remote version.
+If you would run this code, you'll notice that if the user doesn't respond
within the task's interval, a new task is started, causing an ever increasing
number of tasks to be waiting for user input in case an update is available.
Currently, due to the current implementation of Ace's scheduler, there is no
way of disabling this behavior (although it is not really difficult to resolve
this problem locally in your task).
+
+## Taking it a step furtherâ¦
+
+So far, we've reused the `DeploymentService` facade from Ace to tweak the
update process of the management agent a bit. However, there still some magic
going on in the installation of newer versions (all the logic behind
`DeploymentService#installVersion`).Â
+Let's explore this method a bit more into detail. The `installVersion` method
roughly (some details are left out for brevity) has the following
implementation:
+
+ #!java
+ Â Â Â // injected by Felix'Â dependency manager
+ Â Â Â volatile Deployment m_deployer;
+ Â Â Â volatile EventAdmin m_eventAdmin;
+ Â Â Â volatile Identification m_identification;
+ Â Â Â // denotes the host + port where Ace is listening, e.g.
http://192.168.1.1:8080/
+   final String m_host;
+
+ Â Â Â /**
+ Â Â Â Â * @see
org.apache.ace.deployment.service.impl.DeploymentServiceImpl#installVersion
+ Â Â Â Â */
+    public void installVersion(Version highestRemoteVersion, Version
highestLocalVersion) throws Exception {
+ Â Â Â Â Â Â Â InputStream inputStream = null;
+ Â Â Â Â Â
+ Â Â Â Â Â Â Â try {
+ Â Â Â Â Â Â Â Â Â Â Â String version = highestRemoteVersion.toString();
+ Â Â Â Â Â Â Â Â Â Â Â URL baseURL =Â new URL(m_host, "deployment/" +
m_identification.getID() + "/versions/");
+ Â Â Â Â Â Â Â if (highestLocalVersion != null) {
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â version += "?current=" +
highestLocalVersion.toString();
+ Â Â Â Â Â Â Â Â Â Â Â }
+ Â Â Â Â Â Â Â Â Â Â URL dataURL = new URL(baseURL, version);
+ Â Â Â Â Â Â inputStream = dataURL.openStream();
+
+ Â Â Â Â Â Â Â // Post event for audit log
+       String topic = "org/apache/ace/deployment/INSTALL";
+ Â Â Â Â Â Â Â Â Â Â Â m_eventAdmin.postEvent(createEvent(topic, version,
dataURL));
+
+ Â Â Â Â Â Â Â Â Â Â Â m_deployer.install(inputStream);
+ Â Â Â Â Â Â Â }
+ Â Â Â Â Â Â Â finally {
+ Â Â Â Â Â Â Â Â Â Â Â if (inputStream != null) {
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â try {
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â inputStream.close();
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â }
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â catch (Exception ex) {
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â // Not much we can do.
+ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â }
+ Â Â Â Â Â Â Â Â Â Â Â }
+ Â Â Â Â Â Â Â }
+
+Basically, the method builds up an URL to access the deployment service of
Ace. Through this URL, the deployment-service creates a deployment package
containing the changed artifacts between the given local and remote version.
The `InputStream` containing this deployment package is given to the
`Deployment` service (a facade to the standard DeploymentAdmin service) to be
installed. Note that if the installation of the deployment package fails, an
exception is thrown. As mentioned earlier, the installation of deployment
packages is done in a "all or nothing" strategy, meaning that if it fails, all
changes are reverted. For more details on this, you can read the
DeploymentAdmin specification (see [2], chapter 114).
+Aside the actual installation of the deployment package, an event is also
posted to keep track of this installation. This event is picked up by the
`AuditLog` service of Ace to track all changes made to the management agent
(_one can argue whether this shouldn't be a responsibility of the Deployment
facade_).Â
+
+Now we have seen how the installation of deployment packages is implemented in
Ace, we can even tweak that process a bit. For example, we could first download
the deployment package entirely to a temporary location, and install it from
there. Or, as we have access to the deployment package, we could also provide
the user additional information about its contents, perhaps showing a change
log or a summary of its contents, before installing it.
+
+## References
+
+1. Ace subversion, http://svn.apache.org/repos/asf/ace/;
+2. OSGi 4.2 compendium
specification, http://www.osgi.org/Download/Release4V42.
+
Propchange:
ace/site/trunk/content/dev-doc/design/ace-deployment-strategies.mdtext
------------------------------------------------------------------------------
svn:eol-style = native
Added:
ace/site/trunk/content/dev-doc/design/deployment_strategy_classdiagram.svg
URL:
http://svn.apache.org/viewvc/ace/site/trunk/content/dev-doc/design/deployment_strategy_classdiagram.svg?rev=1327406&view=auto
==============================================================================
--- ace/site/trunk/content/dev-doc/design/deployment_strategy_classdiagram.svg
(added)
+++ ace/site/trunk/content/dev-doc/design/deployment_strategy_classdiagram.svg
Wed Apr 18 07:46:56 2012
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.0//EN'
+ 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd'>
+<svg fill-opacity="1" xmlns:xlink="http://www.w3.org/1999/xlink"
color-rendering="auto" color-interpolation="auto" stroke="black"
text-rendering="auto" stroke-linecap="square" width="640"
stroke-miterlimit="10" stroke-opacity="1" shape-rendering="auto" fill="black"
stroke-dasharray="none" font-weight="normal" stroke-width="1" viewBox="90 0 640
380" height="380" xmlns="http://www.w3.org/2000/svg"
font-family="'Dialog'" font-style="normal" stroke-linejoin="miter"
font-size="12" stroke-dashoffset="0" image-rendering="auto"
+><!--Generated by the Batik Graphics2D SVG Generator--><defs id="genericDefs"
+ /><g
+ ><g fill="white" font-family="sans-serif" transform="translate(110,20)"
stroke="white"
+ ><rect x="0" width="109" height="59" y="0" opacity="0" stroke="none"
+ /><rect fill="none" x="0" width="109" height="59" y="0"
stroke="rgb(136,136,136)"
+ /><text fill="rgb(136,136,136)" x="24" xml:space="preserve" y="15"
stroke="none"
+ >«interface»</text
+ ><text fill="rgb(136,136,136)" x="28" xml:space="preserve" y="30"
stroke="none"
+ >Scheduler</text
+ ><line y2="33" fill="none" x1="0" x2="109" stroke="rgb(136,136,136)"
y1="33"
+ /><line transform="translate(80,0)" fill="none" x1="30" x2="140" y1="30"
y2="30" stroke="rgb(136,136,136)"
+ /><line transform="translate(80,0)" fill="none" x1="140" x2="250"
y1="30" y2="30" stroke="rgb(136,136,136)"
+ /><line transform="translate(80,0)" fill="none" x1="30" x2="40" y1="30"
y2="25" stroke="rgb(136,136,136)"
+ /><line transform="translate(80,0)" fill="none" x1="30" x2="40" y1="30"
y2="35" stroke="rgb(136,136,136)"
+ /><polygon points=" 30 30 40 25 50 30 40 35" transform="translate(80,0)"
stroke="none"
+ /><polygon fill="none" transform="translate(80,0)" points=" 30 30 40 25
50 30 40 35" stroke="rgb(136,136,136)"
+ /><text fill="rgb(136,136,136)" x="234" xml:space="preserve" y="46"
transform="translate(80,0)" stroke="none"
+ >*</text
+ ><rect x="0" y="0" transform="translate(190,130)" width="189"
height="59" opacity="0" stroke="none"
+ /><rect x="0" y="0" transform="translate(190,130)" fill="none"
width="189" height="59" stroke="black"
+ /><text fill="black" x="30" xml:space="preserve" y="15"
transform="translate(190,130)" stroke="none"
+ >DeploymentCheckTask</text
+ ><line transform="translate(190,130)" fill="none" x1="0" x2="189"
y1="18" y2="18" stroke="black"
+ /><rect x="0" y="0" transform="translate(410,130)" width="189"
height="59" opacity="0" stroke="none"
+ /><rect x="0" y="0" transform="translate(410,130)" fill="none"
width="189" height="59" stroke="black"
+ /><text fill="black" x="27" xml:space="preserve" y="15"
transform="translate(410,130)" stroke="none"
+ >DeploymentUpdateTask</text
+ ><line transform="translate(410,130)" fill="none" x1="0" x2="189"
y1="18" y2="18" stroke="black"
+ /></g
+ ><g stroke-dasharray="8,5" stroke-miterlimit="5" font-family="sans-serif"
transform="translate(360,50)" stroke-linecap="butt"
+ ><line y2="70" fill="none" x1="150" x2="150" y1="30"
+ /><line y2="70" fill="none" x1="150" x2="90" y1="70"
+ /><line y2="70" fill="none" x1="90" x2="30" y1="70"
+ /><line y2="100" fill="none" x1="30" x2="30" y1="70"
+ /><line stroke-linecap="square" fill="none" x1="150" x2="155" y1="30"
y2="40" stroke-dasharray="none" stroke-miterlimit="10"
+ /><line stroke-linecap="square" fill="none" x1="150" x2="145" y1="30"
y2="40" stroke-dasharray="none" stroke-miterlimit="10"
+ /></g
+ ><g fill="white" font-family="sans-serif" transform="translate(360,50)"
stroke="white"
+ ><polygon points=" 150 30 155 40 145 40" stroke="none"
+ /><polygon fill="none" points=" 150 30 155 40 145 40" stroke="black"
+ /></g
+ ><g stroke-dasharray="8,5" stroke-miterlimit="5" font-family="sans-serif"
transform="translate(480,50)" stroke-linecap="butt"
+ ><line y2="70" fill="none" x1="30" x2="30" y1="30"
+ /><line y2="70" fill="none" x1="30" x2="85" y1="70"
+ /><line y2="70" fill="none" x1="85" x2="140" y1="70"
+ /><line y2="100" fill="none" x1="140" x2="140" y1="70"
+ /><line stroke-linecap="square" fill="none" x1="30" x2="35" y1="30"
y2="40" stroke-dasharray="none" stroke-miterlimit="10"
+ /><line stroke-linecap="square" fill="none" x1="30" x2="25" y1="30"
y2="40" stroke-dasharray="none" stroke-miterlimit="10"
+ /></g
+ ><g fill="white" font-family="sans-serif" transform="translate(480,50)"
stroke="white"
+ ><polygon points=" 30 30 35 40 25 40" stroke="none"
+ /><polygon fill="none" points=" 30 30 35 40 25 40" stroke="black"
+ /><line transform="translate(-10,130)" fill="none" x1="150" x2="150"
y1="30" y2="80" stroke="black"
+ /><line transform="translate(-10,130)" fill="none" x1="150" x2="90"
y1="80" y2="80" stroke="black"
+ /><line transform="translate(-10,130)" fill="none" x1="90" x2="30"
y1="80" y2="80" stroke="black"
+ /><line transform="translate(-10,130)" fill="none" x1="30" x2="30"
y1="80" y2="120" stroke="black"
+ /><line transform="translate(-10,130)" fill="none" x1="150" x2="155"
y1="30" y2="40" stroke="black"
+ /><line transform="translate(-10,130)" fill="none" x1="150" x2="145"
y1="30" y2="40" stroke="black"
+ /><polygon points=" 150 30 155 40 150 50 145 40"
transform="translate(-10,130)" stroke="none"
+ /><polygon fill="none" transform="translate(-10,130)" points=" 150 30
155 40 150 50 145 40" stroke="black"
+ /><text fill="black" x="28" xml:space="preserve" y="118"
transform="translate(-10,130)" stroke="none"
+ > 1</text
+ ><line transform="translate(-120,130)" fill="none" x1="30" x2="30"
y1="30" y2="80" stroke="black"
+ /><line transform="translate(-120,130)" fill="none" x1="30" x2="85"
y1="80" y2="80" stroke="black"
+ /><line transform="translate(-120,130)" fill="none" x1="85" x2="140"
y1="80" y2="80" stroke="black"
+ /><line transform="translate(-120,130)" fill="none" x1="140" x2="140"
y1="80" y2="120" stroke="black"
+ /><line transform="translate(-120,130)" fill="none" x1="30" x2="35"
y1="30" y2="40" stroke="black"
+ /><line transform="translate(-120,130)" fill="none" x1="30" x2="25"
y1="30" y2="40" stroke="black"
+ /><polygon points=" 30 30 35 40 30 50 25 40"
transform="translate(-120,130)" stroke="none"
+ /><polygon fill="none" transform="translate(-120,130)" points=" 30 30 35
40 30 50 25 40" stroke="black"
+ /><text fill="black" x="138" xml:space="preserve" y="118"
transform="translate(-120,130)" stroke="none"
+ > 1</text
+ ><rect x="0" y="0" transform="translate(-40,-30)" width="139"
height="59" opacity="0" stroke="none"
+ /><rect x="0" y="0" transform="translate(-40,-30)" fill="none"
width="139" height="59" stroke="black"
+ /><text fill="black" x="39" xml:space="preserve" y="15"
transform="translate(-40,-30)" stroke="none"
+ >«interface»</text
+ ><text fill="black" x="44" xml:space="preserve" y="30"
transform="translate(-40,-30)" stroke="none"
+ >Runnable</text
+ ><line transform="translate(-40,-30)" fill="none" x1="0" x2="139"
y1="33" y2="33" stroke="black"
+ /><rect x="0" y="0" transform="translate(-50,250)" width="139"
height="59" opacity="0" stroke="none"
+ /><rect x="0" y="0" transform="translate(-50,250)" fill="none"
width="139" height="59" stroke="black"
+ /><text fill="black" x="39" xml:space="preserve" y="15"
transform="translate(-50,250)" stroke="none"
+ >«interface»</text
+ ><text fill="black" x="16" xml:space="preserve" y="30"
transform="translate(-50,250)" stroke="none"
+ >DeploymentService</text
+ ><line transform="translate(-50,250)" fill="none" x1="0" x2="139"
y1="33" y2="33" stroke="black"
+ /></g
+ ></g
+></svg
+>
Propchange:
ace/site/trunk/content/dev-doc/design/deployment_strategy_classdiagram.svg
------------------------------------------------------------------------------
svn:eol-style = native