You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ace.apache.org by ma...@apache.org on 2012/07/05 14:10:06 UTC

svn commit: r1357570 [22/34] - in /ace/sandbox/marrs: cnf/ cnf/ext/ cnf/lib/ cnf/releaserepo/ cnf/repo/ cnf/repo/.obrcache/ cnf/repo/.obrcache/http%3A%2F%2Fbundles.bndtools.org.s3.amazonaws.com%2Fcom.jcraft.jsch/ cnf/repo/.obrcache/http%3A%2F%2Fbundles...

Added: ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RESTClientServlet.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RESTClientServlet.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RESTClientServlet.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RESTClientServlet.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,691 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.rest;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.ace.client.repository.RepositoryObject;
+import org.apache.ace.client.repository.SessionFactory;
+import org.apache.ace.client.repository.stateful.StatefulTargetObject;
+import org.apache.ace.log.LogEvent;
+import org.apache.felix.dm.Component;
+import org.apache.felix.dm.DependencyManager;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.log.LogService;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+
+/**
+ * Servlet that offers a REST client API.
+ */
+public class RESTClientServlet extends HttpServlet implements ManagedService {
+    
+    /** Alias that redirects to the latest version automatically. */
+    private static final String LATEST_FOLDER = "latest";
+    /** Name of the folder where working copies are kept. */
+    private static final String WORK_FOLDER = "work";
+    /** A boolean denoting whether or not authentication is enabled. */
+    private static final String KEY_USE_AUTHENTICATION = "authentication.enabled";
+    /** URL of the repository to talk to. */
+    private static final String KEY_REPOSITORY_URL = "repository.url";
+    /** URL of the OBR to talk to. */
+    private static final String KEY_OBR_URL = "obr.url";
+    /** Name of the customer. */
+    private static final String KEY_CUSTOMER_NAME = "customer.name";
+    /** Name of the store repository. */
+    private static final String KEY_STORE_REPOSITORY_NAME = "store.repository.name";
+    /** Name of the distribution repository. */
+    private static final String KEY_DISTRIBUTION_REPOSITORY_NAME = "distribution.repository.name";
+    /** Name of the deployment repository. */
+    private static final String KEY_DEPLOYMENT_REPOSITORY_NAME = "deployment.repository.name";
+    /** Name of the user to log in as, in case no actual authentication is used. */
+    private static final String KEY_USER_NAME = "user.name";
+    /** The action name for approving targets. */
+    private static final String ACTION_APPROVE = "approve";
+    /** The action name for registering targets. */
+    private static final String ACTION_REGISTER = "register";
+    /** The action name for reading audit events. */
+    private static final String ACTION_AUDITEVENTS = "auditEvents";
+
+    private static long m_sessionID = 1;
+
+    private volatile LogService m_logger;
+    private volatile DependencyManager m_dm;
+    private volatile SessionFactory m_sessionFactory;
+
+    private final Map<String, Workspace> m_workspaces;
+    private final Map<String, Component> m_workspaceComponents;
+    private final Gson m_gson;
+    
+    private boolean m_useAuthentication;
+    private String m_repositoryURL;
+    private String m_obrURL;
+    private String m_customerName;
+    private String m_storeRepositoryName;
+    private String m_targetRepositoryName;
+    private String m_deploymentRepositoryName;
+    private String m_serverUser;
+
+    /**
+     * Creates a new {@link RESTClientServlet} instance.
+     */
+    public RESTClientServlet() {
+        m_gson = (new GsonBuilder())
+            .registerTypeHierarchyAdapter(RepositoryObject.class, new RepositoryObjectSerializer())
+            .registerTypeHierarchyAdapter(LogEvent.class, new LogEventSerializer())
+            .create();
+        
+        m_workspaces = new HashMap<String, Workspace>();
+        m_workspaceComponents = new HashMap<String, Component>();
+    }
+
+    public void updated(Dictionary properties) throws ConfigurationException {
+        // First check whether all mandatory configuration keys are available...
+        String useAuth = getProperty(properties, KEY_USE_AUTHENTICATION);
+        if (useAuth == null || !("true".equalsIgnoreCase(useAuth) || "false".equalsIgnoreCase(useAuth))) {
+            throw new ConfigurationException(KEY_USE_AUTHENTICATION, "Missing or invalid value!");
+        }
+
+        // Note that configuration changes are only applied to new work areas, started after the
+        // configuration was changed. No attempt is done to "fix" existing work areas, although we
+        // might consider flushing/invalidating them.
+        synchronized (m_workspaces) {
+            m_useAuthentication = Boolean.valueOf(useAuth);
+            m_repositoryURL = getProperty(properties, KEY_REPOSITORY_URL, "http://localhost:8080/repository");
+            m_obrURL = getProperty(properties, KEY_OBR_URL, "http://localhost:8080/obr");
+            m_customerName = getProperty(properties, KEY_CUSTOMER_NAME, "apache");
+            m_storeRepositoryName = getProperty(properties, KEY_STORE_REPOSITORY_NAME, "shop");
+            m_targetRepositoryName = getProperty(properties, KEY_DISTRIBUTION_REPOSITORY_NAME, "target");
+            m_deploymentRepositoryName = getProperty(properties, KEY_DEPLOYMENT_REPOSITORY_NAME, "deployment");
+            m_serverUser = getProperty(properties, KEY_USER_NAME, "d");
+        }
+    }
+
+    /**
+     * Builds a URL path from the supplied elements. Each individual element is URL encoded.
+     * 
+     * @param elements the elements
+     * @return the URL path
+     */
+    String buildPathFromElements(String... elements) {
+        StringBuilder result = new StringBuilder();
+        for (String element : elements) {
+            if (result.length() > 0) {
+                result.append('/');
+            }
+            result.append(urlEncode(element));
+        }
+        return result.toString();
+    }
+
+    /**
+     * Returns the separate path parts from the request, and URL decodes them.
+     * 
+     * @param req the request
+     * @return the separate path parts
+     */
+    String[] getPathElements(HttpServletRequest req) {
+        String path = req.getPathInfo();
+        if (path == null) {
+            return new String[0];
+        }
+        if (path.startsWith("/") && path.length() > 1) {
+            path = path.substring(1);
+        }
+        if (path.endsWith("/") && path.length() > 1) {
+            path = path.substring(0, path.length() - 1);
+        }
+        
+        String[] pathElements = path.split("/");
+        for (int i = 0; i < pathElements.length; i++) {
+            pathElements[i] = urlDecode(pathElements[i]);
+        }
+        
+        return pathElements;
+    }
+
+    /**
+     * Helper method to safely obtain a property value from the given dictionary.
+     * 
+     * @param properties the dictionary to retrieve the value from, can be <code>null</code>;
+     * @param key the name of the property to retrieve, cannot be <code>null</code>;
+     * @param defaultValue the default value to return in case the property does not exist, or the given dictionary was <code>null</code>.
+     * @return a property value, can be <code>null</code>.
+     */
+    String getProperty(Dictionary properties, String key, String defaultValue) {
+        String value = getProperty(properties, key);
+        return (value == null) ? defaultValue : value; 
+    }
+
+    /**
+     * Helper method to safely obtain a property value from the given dictionary.
+     * 
+     * @param properties the dictionary to retrieve the value from, can be <code>null</code>;
+     * @param key the name of the property to retrieve, cannot be <code>null</code>.
+     * @return a property value, can be <code>null</code>.
+     */
+    String getProperty(Dictionary properties, String key) {
+        if (properties != null) {
+            Object value = properties.get(key);
+            if (value != null && value instanceof String) {
+                return (String) value;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @see javax.servlet.http.HttpServlet#doDelete(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    @Override
+    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        String[] pathElements = getPathElements(req);
+        if (pathElements == null || pathElements.length < 1 || !WORK_FOLDER.equals(pathElements[0])) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+
+        final String id = pathElements[1];
+
+        Workspace workspace = getWorkspace(id);
+        if (workspace == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Could not find workspace: " + id);
+            return;
+        }
+
+        if (pathElements.length == 2) {
+            removeWorkspace(id, resp);
+        }
+        else if (pathElements.length == 4) {
+            deleteRepositoryObject(workspace, pathElements[2], pathElements[3], resp);
+        }
+        else {
+            // All other path lengths...
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+        }
+    }
+
+    /**
+     * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        String[] pathElements = getPathElements(req);
+        if (pathElements == null || pathElements.length == 0) {
+            // TODO return a list of versions
+            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, "Not implemented: list of versions");
+            return;
+        }
+
+        if (pathElements.length == 1) {
+            if (LATEST_FOLDER.equals(pathElements[0])) {
+                // TODO redirect to latest version
+                // resp.sendRedirect("notImplemented" /* to latest version */);
+                resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, "Not implemented: redirect to latest version");
+                return;
+            }
+            else {
+                // All other paths...
+                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            }
+        }
+        else {
+            // path elements of length > 1...
+            final String id = pathElements[1];
+
+            Workspace workspace = getWorkspace(id);
+            if (workspace == null) {
+                resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Could not find workspace: " + id);
+                return;
+            }
+
+            if (pathElements.length == 2) {
+                // TODO this should be the current set of repository objects?!
+                JsonArray result = new JsonArray();
+                result.add(new JsonPrimitive(Workspace.ARTIFACT));
+                result.add(new JsonPrimitive(Workspace.ARTIFACT2FEATURE));
+                result.add(new JsonPrimitive(Workspace.FEATURE));
+                result.add(new JsonPrimitive(Workspace.FEATURE2DISTRIBUTION));
+                result.add(new JsonPrimitive(Workspace.DISTRIBUTION));
+                result.add(new JsonPrimitive(Workspace.DISTRIBUTION2TARGET));
+                result.add(new JsonPrimitive(Workspace.TARGET));
+                resp.getWriter().println(m_gson.toJson(result));
+                return;
+            }
+            else if (pathElements.length == 3) {
+                listRepositoryObjects(workspace, pathElements[2], resp);
+            }
+            else if (pathElements.length == 4) {
+                readRepositoryObject(workspace, pathElements[2], pathElements[3], resp);
+            }
+            else if (pathElements.length == 5) {
+                handleWorkspaceAction(workspace, pathElements[2], pathElements[3], pathElements[4], req, resp);
+            }
+            else {
+                // All other path lengths...
+                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            }
+        }
+    }
+
+    /**
+     * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    @Override
+    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        String[] pathElements = getPathElements(req);
+        if (pathElements == null || pathElements.length < 1 || !WORK_FOLDER.equals(pathElements[0])) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+
+        if (pathElements.length == 1) {
+            createWorkspace(req, resp);
+        }
+        else {
+            // more than one path elements...
+            Workspace workspace = getWorkspace(pathElements[1]);
+            if (workspace == null) {
+                resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Could not find workspace: " + pathElements[1]);
+                return;
+            }
+
+            if (pathElements.length == 2) {
+                // Possible commit of workspace...
+                commitWorkspace(workspace, resp);
+            }
+            else if (pathElements.length == 3) {
+                // Possible repository object creation...
+                RepositoryValueObject data = getRepositoryValueObject(req);
+                createRepositoryObject(workspace, pathElements[2], data, resp);
+            }
+            else if (pathElements.length == 5) {
+                // Possible workspace action...
+                performWorkspaceAction(workspace, pathElements[2], pathElements[3], pathElements[4], resp);
+            }
+            else {
+                // All other path lengths...
+                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            }
+        }
+    }
+
+    @Override
+    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        String[] pathElements = getPathElements(req);
+        if (pathElements == null || pathElements.length != 4 || !WORK_FOLDER.equals(pathElements[0])) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+
+        Workspace workspace = getWorkspace(pathElements[1]);
+        if (workspace == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Could not find workspace: " + pathElements[1]);
+            return;
+        }
+
+        RepositoryValueObject data = getRepositoryValueObject(req);
+        updateRepositoryObject(workspace, pathElements[2], pathElements[3], data, resp);
+    }
+
+    /**
+     * Commits the given workspace.
+     * 
+     * @param workspace the workspace to commit;
+     * @param resp the servlet repsonse to write the response data to.
+     * @throws IOException in case of I/O errors.
+     */
+    private void commitWorkspace(Workspace workspace, HttpServletResponse resp) throws IOException {
+        try {
+            workspace.commit();
+        }
+        catch (Exception e) {
+            m_logger.log(LogService.LOG_WARNING, "Failed to commit workspace!", e);
+            resp.sendError(HttpServletResponse.SC_CONFLICT, "Commit failed: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Creates a new repository object.
+     * 
+     * @param workspace the workspace to create the new repository object in;
+     * @param entityType the type of repository object to create;
+     * @param data the repository value object to use as content for the to-be-created repository object;
+     * @param resp the servlet response to write the response data to.
+     * @throws IOException in case of I/O errors.
+     */
+    private void createRepositoryObject(Workspace workspace, String entityType, RepositoryValueObject data, HttpServletResponse resp) throws IOException {
+        try {
+            RepositoryObject object = workspace.addRepositoryObject(entityType, data.attributes, data.tags);
+
+            resp.sendRedirect(buildPathFromElements(WORK_FOLDER, workspace.getSessionID(), entityType, object.getDefinition()));
+        }
+        catch (IllegalArgumentException e) {
+            m_logger.log(LogService.LOG_WARNING, "Failed to add entity of type: " + entityType, e);
+            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Could not add entity of type " + entityType);
+        }
+    }
+
+    /**
+     * Creates a new workspace.
+     * 
+     * @param resp the servlet response to write the response data to.
+     * @throws IOException in case of I/O errors.
+     */
+    private void createWorkspace(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
+        // TODO get data from post body (if no data, assume latest??) -> for now always assume latest
+        final String sessionID;
+        final Workspace workspace;
+        final Component component;
+
+        synchronized (m_workspaces) {
+            sessionID = "rest-" + m_sessionID++;
+            workspace = new Workspace(sessionID, m_repositoryURL, m_obrURL, m_customerName, m_storeRepositoryName, m_targetRepositoryName, m_deploymentRepositoryName, m_useAuthentication, m_serverUser);
+            m_workspaces.put(sessionID, workspace);
+
+            component = m_dm.createComponent().setImplementation(workspace);
+            m_workspaceComponents.put(sessionID, component);
+        }
+        m_sessionFactory.createSession(sessionID);
+        m_dm.add(component);
+
+        if (!workspace.login(req)) {
+            resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+        } else {
+            resp.sendRedirect(buildPathFromElements(WORK_FOLDER, sessionID));
+        }
+    }
+
+    /**
+     * Deletes a repository object from the current workspace.
+     * 
+     * @param workspace the workspace to perform the action for;
+     * @param entityType the type of entity to apply the action to;
+     * @param entityId the identification of the entity to apply the action to;
+     * @param resp the servlet response to write the response data to.
+     * @throws IOException in case of I/O errors.
+     */
+    private void deleteRepositoryObject(Workspace workspace, String entityType, String entityId, HttpServletResponse resp) throws IOException {
+        try {
+            workspace.deleteRepositoryObject(entityType, entityId);
+        }
+        catch (IllegalArgumentException e) {
+            m_logger.log(LogService.LOG_WARNING, "Failed to delete repository object!", e);
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Repository object of type " + entityType + " and identity " + entityId + " not found.");
+        }
+    }
+
+    /**
+     * Interprets the given request-data as JSON-data and converts it to a {@link RepositoryValueObject} instance.
+     * 
+     * @param request the servlet request data to interpret;
+     * @return a {@link RepositoryValueObject} representation of the given request-data, never <code>null</code>.
+     * @throws IOException in case of I/O errors, or in case the JSON parsing failed.
+     */
+    private RepositoryValueObject getRepositoryValueObject(HttpServletRequest request) throws IOException {
+        try {
+            return m_gson.fromJson(request.getReader(), RepositoryValueObject.class);
+        }
+        catch (JsonParseException e) {
+            m_logger.log(LogService.LOG_WARNING, "Invalid repository object data!", e);
+            throw new IOException("Unable to parse repository object!", e);
+        }
+    }
+
+    /**
+     * Returns a workspace by its identification.
+     * 
+     * @param id the (session) identifier of the workspace to return.
+     * @return the workspace with requested ID, or <code>null</code> if no such workspace exists.
+     */
+    private Workspace getWorkspace(String id) {
+        Workspace workspace;
+        synchronized (m_workspaces) {
+            workspace = m_workspaces.get(id);
+        }
+        return workspace;
+    }
+
+    /**
+     * Performs an idempotent action on an repository object for the given workspace.
+     * 
+     * @param workspace the workspace to perform the action for;
+     * @param entityType the type of entity to apply the action to;
+     * @param entityId the identification of the entity to apply the action to;
+     * @param action the (name of the) action to apply;
+     * @param req the servlet request to read the request data from;
+     * @param resp the servlet response to write the response data to.
+     * @throws IOException
+     */
+    private void handleWorkspaceAction(Workspace workspace, String entityType, String entityId, String action, HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        RepositoryObject repositoryObject = workspace.getRepositoryObject(entityType, entityId);
+        if (repositoryObject == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Repository object of type " + entityType + " and identity " + entityId + " not found.");
+            return;
+        }
+
+        if (Workspace.TARGET.equals(entityType) && ACTION_APPROVE.equals(action)) {
+            resp.getWriter().println(m_gson.toJson(((StatefulTargetObject) repositoryObject).getStoreState()));
+        }
+        else if (Workspace.TARGET.equals(entityType) && ACTION_REGISTER.equals(action)) {
+            resp.getWriter().println(m_gson.toJson(((StatefulTargetObject) repositoryObject).getRegistrationState()));
+        }
+        else if (Workspace.TARGET.equals(entityType) && ACTION_AUDITEVENTS.equals(action)) {
+            StatefulTargetObject target = (StatefulTargetObject) repositoryObject;
+            List<LogEvent> events = target.getAuditEvents();
+
+            String startValue = req.getParameter("start");
+            String maxValue = req.getParameter("max");
+
+            int start = (startValue == null) ? 0 : Integer.parseInt(startValue);
+            // ACE-237: ensure the start-value is a correctly bounded positive integer...
+            start = Math.max(0, Math.min(events.size() - 1, start));
+
+            int max = (maxValue == null) ? 100 : Integer.parseInt(maxValue);
+            // ACE-237: ensure the max- & end-values are correctly bounded...
+            max = Math.max(1, max);
+
+            int end = Math.min(events.size(), start + max);
+
+            List<LogEvent> selection = events.subList(start, end);
+            resp.getWriter().println(m_gson.toJson(selection));
+        }
+        else {
+            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unknown action for " + entityType);
+        }
+    }
+
+    /**
+     * Returns the identifiers of all repository objects of a given type.
+     * 
+     * @param workspace the workspace to read the repository objects from;
+     * @param entityType the type of repository objects to read;
+     * @param resp the servlet response to write the response data to.
+     * @throws IOException in case of I/O problems.
+     */
+    private void listRepositoryObjects(Workspace workspace, String entityType, HttpServletResponse resp) throws IOException {
+        // TODO add a feature to filter the list that is returned (query, paging, ...)
+        List<RepositoryObject> objects = workspace.getRepositoryObjects(entityType);
+
+        JsonArray result = new JsonArray();
+        for (RepositoryObject ro : objects) {
+            String identity = ro.getDefinition();
+            if (identity != null) {
+                result.add(new JsonPrimitive(urlEncode(identity)));
+            }
+        }
+
+        resp.getWriter().println(m_gson.toJson(result));
+    }
+
+    /**
+     * Performs a non-idempotent action on an repository object for the given workspace.
+     * 
+     * @param workspace the workspace to perform the action for;
+     * @param entityType the type of entity to apply the action to;
+     * @param entityId the identification of the entity to apply the action to;
+     * @param action the (name of the) action to apply;
+     * @param resp the servlet response to write the response data to.
+     * @throws IOException in case of I/O errors.
+     */
+    private void performWorkspaceAction(Workspace workspace, String entityType, String entityId, String action, HttpServletResponse resp) throws IOException {
+        RepositoryObject repositoryObject = workspace.getRepositoryObject(entityType, entityId);
+        if (repositoryObject == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Repository object of type " + entityType + " and identity " + entityId + " not found.");
+            return;
+        }
+
+        if (Workspace.TARGET.equals(entityType) && ACTION_APPROVE.equals(action)) {
+            StatefulTargetObject sto = workspace.approveTarget((StatefulTargetObject) repositoryObject);
+
+            // Respond with the current store state...
+            resp.getWriter().println(m_gson.toJson(sto.getStoreState()));
+        }
+        else if (Workspace.TARGET.equals(entityType) && ACTION_REGISTER.equals(action)) {
+            StatefulTargetObject sto = workspace.registerTarget((StatefulTargetObject) repositoryObject);
+            if (sto == null) {
+                resp.sendError(HttpServletResponse.SC_CONFLICT, "Target already registered: " + entityId);
+            }
+            else {
+                // Respond with the current registration state...
+                resp.getWriter().println(m_gson.toJson(sto.getRegistrationState()));
+            }
+        }
+        else {
+            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unknown action for " + entityType);
+        }
+    }
+
+    /**
+     * Reads a single repository object and returns a JSON representation of it.
+     * 
+     * @param workspace the workspace to read the repository object from;
+     * @param entityType the type of repository object to read;
+     * @param entityId the identifier of the repository object to read;
+     * @param resp the servlet response to write the response data to.
+     * @throws IOException in case of I/O problems.
+     */
+    private void readRepositoryObject(Workspace workspace, String entityType, String entityId, HttpServletResponse resp) throws IOException {
+        RepositoryObject repositoryObject = workspace.getRepositoryObject(entityType, entityId);
+        if (repositoryObject == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Repository object of type " + entityType + " and identity " + entityId + " not found.");
+        }
+        else {
+            resp.getWriter().println(m_gson.toJson(repositoryObject));
+        }
+    }
+
+    /**
+     * Removes the workspace with the given identifier.
+     * 
+     * @param id the identifier of the workspace to remove; 
+     * @param resp the servlet response to write the response data to.
+     * @throws IOException in case of I/O problems.
+     */
+    private void removeWorkspace(final String id, HttpServletResponse resp) throws IOException {
+        final Workspace workspace;
+        final Component component;
+
+        synchronized (m_workspaces) {
+            workspace = m_workspaces.remove(id);
+            component = m_workspaceComponents.remove(id);
+        }
+
+        if ((workspace != null) && (component != null)) {
+            workspace.destroy();
+
+            m_dm.remove(component);
+            m_sessionFactory.destroySession(id);
+        }
+        else {
+            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Could not delete work area.");
+        }
+    }
+
+    /**
+     * Updates an existing repository object.
+     * 
+     * @param workspace the workspace to update the repository object in;
+     * @param entityType the type of repository object to update;
+     * @param entityId the identifier of the repository object to update;
+     * @param data the repository value object to use as content for the to-be-updated repository object;
+     * @param resp the servlet response to write the response data to.
+     * @throws IOException in case of I/O errors.
+     */
+    private void updateRepositoryObject(Workspace workspace, String entityType, String entityId, RepositoryValueObject data, HttpServletResponse resp) throws IOException {
+        try {
+            workspace.updateObjectWithData(entityType, entityId, data);
+
+            resp.sendRedirect(buildPathFromElements(WORK_FOLDER, workspace.getSessionID(), entityType, entityId));
+        }
+        catch (IllegalArgumentException e) {
+            m_logger.log(LogService.LOG_WARNING, "Failed to update entity of type: " + entityType, e);
+            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Could not update entity of type " + entityType);
+        }
+    }
+
+    /**
+     * URL decodes a given element.
+     * 
+     * @param element the element to decode, cannot be <code>null</code>.
+     * @return the decoded element, never <code>null</code>.
+     */
+    private String urlDecode(String element) {
+        try {
+            return URLDecoder.decode(element.replaceAll("%20", "\\+"), "UTF-8");
+        }
+        catch (UnsupportedEncodingException e) {
+            // ignored on purpose, any JVM must support UTF-8
+            return null; // should never occur
+        }
+    }
+
+    /**
+     * URL encodes a given element.
+     * 
+     * @param element the element to encode, cannot be <code>null</code>.
+     * @return the encoded element, never <code>null</code>.
+     */
+    private String urlEncode(String element) {
+        try {
+            return URLEncoder.encode(element, "UTF-8").replaceAll("\\+", "%20");
+        }
+        catch (UnsupportedEncodingException e) {
+            // ignored on purpose, any JVM must support UTF-8
+            return null; // should never occur
+        }
+    }
+}

Added: ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RepositoryObjectSerializer.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RepositoryObjectSerializer.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RepositoryObjectSerializer.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RepositoryObjectSerializer.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.rest;
+
+import java.lang.reflect.Type;
+import java.util.Enumeration;
+
+import org.apache.ace.client.repository.RepositoryObject;
+import org.apache.ace.client.repository.object.ArtifactObject;
+import org.apache.ace.client.repository.object.DeploymentArtifact;
+import org.apache.ace.client.repository.stateful.StatefulTargetObject;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+/**
+ * Provides an object serializer for the entire type hierarchy of {@link RepositoryObject}s.
+ */
+public class RepositoryObjectSerializer implements JsonSerializer<RepositoryObject> {
+
+    /** used in all repository objects. */
+    private static final String DEFINITION = "definition";
+    /** used in all repository objects. */
+    private static final String TAGS = "tags";
+    /** used in all repository objects. */
+    private static final String ATTRIBUTES = "attributes";
+    /** used in stateful target objects only. */
+    private static final String STATE = "state";
+    
+    private static final String REGISTRATION_STATE = "registrationState";
+    private static final String CURRENT_VERSION = "currentVersion";
+    private static final String STORE_STATE = "storeState";
+    private static final String PROVISIONING_STATE = "provisioningState";
+    private static final String IS_REGISTERED = "isRegistered";
+    private static final String NEEDS_APPROVAL = "needsApproval";
+    private static final String AUTO_APPROVE = "autoApprove";
+    private static final String ARTIFACTS_FROM_SHOP = "artifactsFromShop";
+    private static final String ARTIFACTS_FROM_DEPLOYMENT = "artifactsFromDeployment";
+    private static final String LAST_INSTALL_VERSION = "lastInstallVersion";
+    private static final String LAST_INSTALL_SUCCESS = "lastInstallSuccess";
+
+    /**
+     * @see com.google.gson.JsonSerializer#serialize(java.lang.Object, java.lang.reflect.Type, com.google.gson.JsonSerializationContext)
+     */
+    public JsonElement serialize(RepositoryObject repositoryObject, Type featureType, JsonSerializationContext context) {
+        // ACE-164: for stateful target objects we need some special measures to serialize it...
+        if (repositoryObject instanceof StatefulTargetObject) {
+            return serializeStatefulTargetObject((StatefulTargetObject) repositoryObject);
+        }
+
+        // All other repository objects can be simply serialized...
+        return serializeRepositoryObject(repositoryObject);
+    }
+
+    /**
+     * Custom serializer method for {@link StatefulTargetObject}s, as they have state and cannot be accessed
+     * always in the same way as other repository objects. For example, when dealing with unregistered targets,
+     * we cannot ask for the attributes and/or tags of a target.
+     * 
+     * @param targetObject the target object to serialize, cannot be <code>null</code>.
+     * @return a JSON representation of the given target object, never <code>null</code>.
+     */
+    private JsonElement serializeStatefulTargetObject(StatefulTargetObject targetObject) {
+        JsonObject result = new JsonObject();
+        // ACE-243: first all the definition...
+        result.addProperty(DEFINITION, targetObject.getDefinition());
+
+        // then add all attributes
+        JsonObject attr = new JsonObject();
+
+        if (targetObject.isRegistered()) {
+            Enumeration<String> keys = targetObject.getAttributeKeys();
+            while (keys.hasMoreElements()) {
+                String key = keys.nextElement();
+                attr.addProperty(key, targetObject.getAttribute(key));
+            }
+        }
+        else {
+            // Ensure that the ID of the target is always present as attribute...
+            attr.addProperty(StatefulTargetObject.KEY_ID, targetObject.getID());
+        }
+
+        result.add(ATTRIBUTES, attr);
+
+        // then add all tags
+        JsonObject tags = new JsonObject();
+
+        if (targetObject.isRegistered()) {
+            Enumeration<String> keys = targetObject.getTagKeys();
+            while (keys.hasMoreElements()) {
+                String key = keys.nextElement();
+                tags.addProperty(key, targetObject.getTag(key));
+            }
+        }
+
+        result.add(TAGS, tags);
+
+        // finally, if it's a target with state, add that as well
+        JsonObject state = new JsonObject();
+        state.addProperty(REGISTRATION_STATE, targetObject.getRegistrationState().name());
+        state.addProperty(PROVISIONING_STATE, targetObject.getProvisioningState().name());
+        state.addProperty(STORE_STATE, targetObject.getStoreState().name());
+        state.addProperty(CURRENT_VERSION, targetObject.getCurrentVersion());
+        state.addProperty(IS_REGISTERED, Boolean.toString(targetObject.isRegistered()));
+        state.addProperty(NEEDS_APPROVAL, Boolean.toString(targetObject.needsApprove()));
+        state.addProperty(AUTO_APPROVE, Boolean.toString(targetObject.getAutoApprove()));
+
+        JsonArray artifactsFromShop = new JsonArray();
+        for (ArtifactObject a : targetObject.getArtifactsFromShop()) {
+            artifactsFromShop.add(new JsonPrimitive(a.getDefinition()));
+        }
+        state.add(ARTIFACTS_FROM_SHOP, artifactsFromShop);
+
+        JsonArray artifactsFromDeployment = new JsonArray();
+        for (DeploymentArtifact a : targetObject.getArtifactsFromDeployment()) {
+            artifactsFromDeployment.add(new JsonPrimitive(a.getUrl()));
+        }
+        state.add(ARTIFACTS_FROM_DEPLOYMENT, artifactsFromDeployment);
+
+        state.addProperty(LAST_INSTALL_VERSION, targetObject.getLastInstallVersion());
+        state.addProperty(LAST_INSTALL_SUCCESS, targetObject.getLastInstallSuccess());
+
+        /* TODO getLicenses/AssocationsWith might not be that helpful since the data is also available in a different way */
+        /* TODO some of this tends to show up as attributes as well, so we will need to do some filtering there */
+        /* TODO some aspects of the state can be manipulated as well, we need to supply methods for that */
+        result.add(STATE, state);
+
+        return result;
+    }
+
+    /**
+     * Serializes a (non stateful target object) repository object to a JSON representation.
+     * 
+     * @param repositoryObject the repository object to serialize, cannot be <code>null</code>.
+     * @return a JSON representation of the given repository object, never <code>null</code>.
+     */
+    private JsonElement serializeRepositoryObject(RepositoryObject repositoryObject) {
+        JsonObject result = new JsonObject();
+        // ACE-243: first all the definition...
+        result.addProperty(DEFINITION, repositoryObject.getDefinition());
+        
+        // then add all attributes
+        JsonObject attr = new JsonObject();
+        
+        Enumeration<String> keys = repositoryObject.getAttributeKeys();
+        while (keys.hasMoreElements()) {
+            String key = keys.nextElement();
+            attr.addProperty(key, repositoryObject.getAttribute(key));
+        }
+        result.add(ATTRIBUTES, attr);
+        
+        // then add all tags
+        JsonObject tags = new JsonObject();
+
+        keys = repositoryObject.getTagKeys();
+        while (keys.hasMoreElements()) {
+            String key = keys.nextElement();
+            tags.addProperty(key, repositoryObject.getTag(key));
+        }
+        result.add(TAGS, tags);
+        
+        return result;
+    }
+}
\ No newline at end of file

Added: ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RepositoryValueObject.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RepositoryValueObject.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RepositoryValueObject.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/RepositoryValueObject.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.rest;
+
+import java.util.Map;
+
+/**
+ * A value object that is used to store incoming JSON data.
+ */
+public class RepositoryValueObject {
+    public Map<String, String> attributes;
+    public Map<String, String> tags;
+}
\ No newline at end of file

Added: ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/Workspace.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/Workspace.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/Workspace.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.rest/src/org/apache/ace/client/rest/Workspace.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,432 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.rest;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.ace.authentication.api.AuthenticationService;
+import org.apache.ace.client.repository.ObjectRepository;
+import org.apache.ace.client.repository.RepositoryAdmin;
+import org.apache.ace.client.repository.RepositoryAdminLoginContext;
+import org.apache.ace.client.repository.RepositoryObject;
+import org.apache.ace.client.repository.SessionFactory;
+import org.apache.ace.client.repository.repository.Artifact2FeatureAssociationRepository;
+import org.apache.ace.client.repository.repository.ArtifactRepository;
+import org.apache.ace.client.repository.repository.Distribution2TargetAssociationRepository;
+import org.apache.ace.client.repository.repository.DistributionRepository;
+import org.apache.ace.client.repository.repository.Feature2DistributionAssociationRepository;
+import org.apache.ace.client.repository.repository.FeatureRepository;
+import org.apache.ace.client.repository.repository.TargetRepository;
+import org.apache.ace.client.repository.stateful.StatefulTargetObject;
+import org.apache.ace.client.repository.stateful.StatefulTargetRepository;
+import org.apache.felix.dm.Component;
+import org.apache.felix.dm.DependencyManager;
+import org.osgi.service.log.LogService;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+
+public class Workspace {
+    static final String ARTIFACT = "artifact";
+    static final String ARTIFACT2FEATURE = "artifact2feature";
+    static final String FEATURE = "feature";
+    static final String FEATURE2DISTRIBUTION = "feature2distribution";
+    static final String DISTRIBUTION = "distribution";
+    static final String DISTRIBUTION2TARGET = "distribution2target";
+    static final String TARGET = "target";
+    
+    private final String m_sessionID;
+    private final URL m_repositoryURL;
+    private final URL m_obrURL;
+    private final String m_customerName;
+    private final String m_storeRepositoryName;
+    private final String m_distributionRepositoryName;
+    private final String m_deploymentRepositoryName;
+    private final String m_serverUser;
+    private final boolean m_useAuthentication;
+    
+    private volatile AuthenticationService m_authenticationService;
+    private volatile DependencyManager m_manager;
+    private volatile RepositoryAdmin m_repositoryAdmin;
+    private volatile ArtifactRepository m_artifactRepository;
+    private volatile FeatureRepository m_featureRepository;
+    private volatile DistributionRepository m_distributionRepository;
+    private volatile StatefulTargetRepository m_statefulTargetRepository;
+    private volatile Artifact2FeatureAssociationRepository m_artifact2FeatureAssociationRepository;
+    private volatile Feature2DistributionAssociationRepository m_feature2DistributionAssociationRepository;
+    private volatile Distribution2TargetAssociationRepository m_distribution2TargetAssociationRepository;
+    private volatile UserAdmin m_userAdmin;
+    private volatile LogService m_log;
+
+    public Workspace(String sessionID, String repositoryURL, String obrURL, String customerName, String storeRepositoryName, String distributionRepositoryName, String deploymentRepositoryName, boolean useAuthentication, String serverUser) throws MalformedURLException {
+        m_sessionID = sessionID;
+        m_repositoryURL = new URL(repositoryURL);
+        m_obrURL = new URL(obrURL);
+        m_customerName = customerName;
+        m_storeRepositoryName = storeRepositoryName;
+        m_distributionRepositoryName = distributionRepositoryName;
+        m_deploymentRepositoryName = deploymentRepositoryName;
+        m_useAuthentication = useAuthentication;
+        m_serverUser = serverUser;
+    }
+    
+    /**
+     * @return the session ID of this workspace, never <code>null</code>.
+     */
+    public String getSessionID() {
+        return m_sessionID;
+    }
+    
+    private void addSessionDependency(Component component, Class service, boolean isRequired) {
+        component.add(m_manager.createServiceDependency()
+            .setService(service, "(" + SessionFactory.SERVICE_SID + "=" + m_sessionID + ")")
+            .setRequired(isRequired)
+            .setInstanceBound(true)
+        );
+    }
+    
+    private void addDependency(Component component, Class service, boolean isRequired) {
+        component.add(m_manager.createServiceDependency()
+            .setService(service)
+            .setRequired(isRequired)
+            .setInstanceBound(true)
+        );
+    }
+    
+    public void init(Component component) {
+        addSessionDependency(component, RepositoryAdmin.class, true);
+        addSessionDependency(component, ArtifactRepository.class, true);
+        addSessionDependency(component, FeatureRepository.class, true);
+        addSessionDependency(component, DistributionRepository.class, true);
+        addSessionDependency(component, TargetRepository.class, true);
+        addSessionDependency(component, StatefulTargetRepository.class, true);
+        addSessionDependency(component, Artifact2FeatureAssociationRepository.class, true);
+        addSessionDependency(component, Feature2DistributionAssociationRepository.class, true);
+        addSessionDependency(component, Distribution2TargetAssociationRepository.class, true);
+        addDependency(component, AuthenticationService.class, m_useAuthentication);
+        addDependency(component, UserAdmin.class, true);
+        addDependency(component, LogService.class, false);
+    }
+    
+    public void start() {
+    }
+    
+    public void destroy() {
+    }
+    
+    public boolean login(HttpServletRequest request) {
+        try {
+            final User user;
+            if (m_useAuthentication) {
+                // Use the authentication service to authenticate the given request...
+                user = m_authenticationService.authenticate(request);
+            } else {
+                // Use the "hardcoded" user to login with...
+                user = m_userAdmin.getUser("username", m_serverUser);
+            }
+            
+            if (user == null) {
+                // No user obtained through request/fallback scenario; login failed...
+                return false;
+            }
+
+            RepositoryAdminLoginContext context = m_repositoryAdmin.createLoginContext(user);
+            
+            context.setObrBase(m_obrURL)
+                .add(context.createShopRepositoryContext()
+                    .setLocation(m_repositoryURL)
+                    .setCustomer(m_customerName)
+                    .setName(m_storeRepositoryName)
+                    .setWriteable())
+                .add(context.createTargetRepositoryContext()
+                    .setLocation(m_repositoryURL)
+                    .setCustomer(m_customerName)
+                    .setName(m_distributionRepositoryName)
+                    .setWriteable())
+                .add(context.createDeploymentRepositoryContext()
+                    .setLocation(m_repositoryURL)
+                    .setCustomer(m_customerName)
+                    .setName(m_deploymentRepositoryName)
+                    .setWriteable());
+
+            m_repositoryAdmin.login(context);
+            m_repositoryAdmin.checkout();
+        }
+        catch (IOException e) {
+            e.printStackTrace();
+            m_log.log(LogService.LOG_ERROR, "Could not login and checkout. Workspace will probably not work correctly.", e);
+        }
+        
+        return true;
+    }
+    
+    public void checkout() throws IOException {
+        m_repositoryAdmin.checkout();
+    }
+
+    public void commit() throws IOException {
+        m_repositoryAdmin.commit();
+    }
+
+    public RepositoryObject getRepositoryObject(String entityType, String entityId) {
+        return getObjectRepository(entityType).get(entityId);
+    }
+
+    public List<RepositoryObject> getRepositoryObjects(String entityType) {
+        List list = getObjectRepository(entityType).get();
+        if (list != null) {
+            return list;
+        }
+        else {
+            return Collections.EMPTY_LIST;
+        }
+    }
+
+    public RepositoryObject addRepositoryObject(String entityType, Map<String, String> attributes, Map<String, String> tags) throws IllegalArgumentException {
+        if (TARGET.equals(entityType)) {
+            return ((StatefulTargetRepository) getObjectRepository(TARGET)).preregister(attributes, tags);
+        }
+        else {
+            if (ARTIFACT2FEATURE.equals(entityType) || FEATURE2DISTRIBUTION.equals(entityType) || DISTRIBUTION2TARGET.equals(entityType)) {
+
+                String leftAttribute = attributes.get("left");
+                String rightAttribute = attributes.get("right");
+
+                RepositoryObject left = null;
+                if(leftAttribute != null) {
+                    left = getLeft(entityType, leftAttribute);
+                }
+
+                RepositoryObject right = null;
+                if(rightAttribute != null) {
+                    right = getRight(entityType, rightAttribute);
+                }
+
+
+                if (left != null) {
+                    if (left instanceof StatefulTargetObject) {
+                        if (((StatefulTargetObject) left).isRegistered()) {
+                            attributes.put("leftEndpoint", ((StatefulTargetObject) left).getTargetObject().getAssociationFilter(attributes));
+                        }
+                    }
+                    else {
+                        attributes.put("leftEndpoint", left.getAssociationFilter(attributes));
+                    }
+                }
+                if (right != null) {
+                    if (right instanceof StatefulTargetObject) {
+                        if (((StatefulTargetObject) right).isRegistered()) {
+                            attributes.put("rightEndpoint", ((StatefulTargetObject) right).getTargetObject().getAssociationFilter(attributes));
+                        }
+                    }
+                    else {
+                        attributes.put("rightEndpoint", right.getAssociationFilter(attributes));
+                    }
+                }
+            }
+            return getObjectRepository(entityType).create(attributes, tags);
+        }
+    }
+    
+    /**
+     * Approves a given stateful target object.
+     * 
+     * @param targetObject the target object to approve, cannot be <code>null</code>.
+     * @return the approved stateful target object, cannot be <code>null</code>.
+     */
+    public StatefulTargetObject approveTarget(StatefulTargetObject targetObject) {
+        targetObject.approve();
+        return targetObject;
+    }
+
+    /**
+     * Registers a given stateful target object.
+     * 
+     * @param targetObject the target object to register, cannot be <code>null</code>.
+     * @return the registered stateful target object, can be <code>null</code> only if the given target object is already registered.
+     */
+    public StatefulTargetObject registerTarget(StatefulTargetObject targetObject) {
+        if (targetObject.isRegistered()) {
+            return null;
+        }
+        targetObject.register();
+        return targetObject;
+    }
+    
+    public void updateObjectWithData(String entityType, String entityId, RepositoryValueObject valueObject) {
+        RepositoryObject repositoryObject = getRepositoryObject(entityType, entityId);
+        // first handle the attributes
+        for (Entry<String, String> attribute : valueObject.attributes.entrySet()) {
+            String key = attribute.getKey();
+            String value = attribute.getValue();
+            // only add/update the attribute if it actually changed
+            if (!value.equals(repositoryObject.getAttribute(key))) {
+                repositoryObject.addAttribute(key, value);
+            }
+        }
+        Enumeration<String> keys = repositoryObject.getAttributeKeys();
+        while (keys.hasMoreElements()) {
+            String key = keys.nextElement();
+            if (!valueObject.attributes.containsKey(key)) {
+                // TODO since we cannot remove keys right now, we null them
+                repositoryObject.addAttribute(key, null);
+            }
+        }
+        if (ARTIFACT2FEATURE.equals(entityType) || FEATURE2DISTRIBUTION.equals(entityType) || DISTRIBUTION2TARGET.equals(entityType)) {
+            String leftAttribute = repositoryObject.getAttribute("left");
+            String rightAttribute = repositoryObject.getAttribute("right");
+
+            RepositoryObject left = null;
+            if (leftAttribute != null) {
+                left = getLeft(entityType, leftAttribute);
+            }
+
+            RepositoryObject right = null;
+            if (rightAttribute != null) {
+                right = getRight(entityType, rightAttribute);
+            }
+
+            if (left != null) {
+                if (left instanceof StatefulTargetObject) {
+                    if (((StatefulTargetObject) left).isRegistered()) {
+                        repositoryObject.addAttribute("leftEndpoint", ((StatefulTargetObject) left).getTargetObject().getAssociationFilter(getAttributes(((StatefulTargetObject) left).getTargetObject())));
+                    }
+                }
+                else {
+                    repositoryObject.addAttribute("leftEndpoint", left.getAssociationFilter(getAttributes(left)));
+                }
+            }
+            if (right != null) {
+                if (right instanceof StatefulTargetObject) {
+                    if (((StatefulTargetObject) right).isRegistered()) {
+                        repositoryObject.addAttribute("rightEndpoint", ((StatefulTargetObject) right).getTargetObject().getAssociationFilter(getAttributes(((StatefulTargetObject) right).getTargetObject())));
+                    }
+                }
+                else {
+                    repositoryObject.addAttribute("rightEndpoint", right.getAssociationFilter(getAttributes(right)));
+                }
+            }
+        }
+        // now handle the tags in a similar way
+        for (Entry<String, String> attribute : valueObject.tags.entrySet()) {
+            String key = attribute.getKey();
+            String value = attribute.getValue();
+            // only add/update the tag if it actually changed
+            if (!value.equals(repositoryObject.getTag(key))) {
+                repositoryObject.addTag(key, value);
+            }
+        }
+        keys = repositoryObject.getTagKeys();
+        while (keys.hasMoreElements()) {
+            String key = keys.nextElement();
+            if (!valueObject.tags.containsKey(key)) {
+                // TODO since we cannot remove keys right now, we null them
+                repositoryObject.addTag(key, null);
+            }
+        }
+    }
+
+    private Map getAttributes(RepositoryObject object) {
+        Map result = new HashMap();
+        for (Enumeration<String> keys = object.getAttributeKeys(); keys.hasMoreElements();) {
+            String key = keys.nextElement();
+            result.put(key, object.getAttribute(key));
+        }
+        return result;
+    }
+
+    public RepositoryObject getLeft(String entityType, String entityId) {
+        if (ARTIFACT2FEATURE.equals(entityType)) {
+            return getObjectRepository(ARTIFACT).get(entityId);
+        }
+        else if (FEATURE2DISTRIBUTION.equals(entityType)) {
+            return getObjectRepository(FEATURE).get(entityId);
+        }
+        else if (DISTRIBUTION2TARGET.equals(entityType)) {
+            return getObjectRepository(DISTRIBUTION).get(entityId);
+        }
+        else {
+            // throws an exception in case of an illegal type!
+            getObjectRepository(entityType);
+        }
+        return null;
+    }
+
+    public RepositoryObject getRight(String entityType, String entityId) {
+        if (ARTIFACT2FEATURE.equals(entityType)) {
+            return getObjectRepository(FEATURE).get(entityId);
+        }
+        else if (FEATURE2DISTRIBUTION.equals(entityType)) {
+            return getObjectRepository(DISTRIBUTION).get(entityId);
+        }
+        else if (DISTRIBUTION2TARGET.equals(entityType)) {
+            return getObjectRepository(TARGET).get(entityId);
+        }
+        else {
+            // throws an exception in case of an illegal type!
+            getObjectRepository(entityType);
+        }
+        return null;
+    }
+    
+    public void deleteRepositoryObject(String entityType, String entityId) {
+        ObjectRepository objectRepository = getObjectRepository(entityType);
+        RepositoryObject repositoryObject = objectRepository.get(entityId);
+        // ACE-239: avoid null entities being passed in...
+        if (repositoryObject == null) {
+            throw new IllegalArgumentException("Could not find repository object!");
+        }
+
+        objectRepository.remove(repositoryObject);
+    }
+
+    private ObjectRepository getObjectRepository(String entityType) {
+        if (ARTIFACT.equals(entityType)) {
+            return m_artifactRepository;
+        }
+        if (ARTIFACT2FEATURE.equals(entityType)) {
+            return m_artifact2FeatureAssociationRepository;
+        }
+        if (FEATURE.equals(entityType)) {
+            return m_featureRepository;
+        }
+        if (FEATURE2DISTRIBUTION.equals(entityType)) {
+            return m_feature2DistributionAssociationRepository;
+        }
+        if (DISTRIBUTION.equals(entityType)) {
+            return m_distributionRepository;
+        }
+        if (DISTRIBUTION2TARGET.equals(entityType)) {
+            return m_distribution2TargetAssociationRepository;
+        }
+        if (TARGET.equals(entityType)) {
+            return m_statefulTargetRepository;
+        }
+        throw new IllegalArgumentException("Unknown entity type: " + entityType);
+    }
+}

Added: ace/sandbox/marrs/org.apache.ace.client.rest/test-output/Default suite/Default test.html
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.rest/test-output/Default%20suite/Default%20test.html?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.rest/test-output/Default suite/Default test.html (added)
+++ ace/sandbox/marrs/org.apache.ace.client.rest/test-output/Default suite/Default test.html Thu Jul  5 12:09:30 2012
@@ -0,0 +1,89 @@
+<html>
+<head>
+<title>TestNG:  Default test</title>
+<link href="../testng.css" rel="stylesheet" type="text/css" />
+<link href="../my-testng.css" rel="stylesheet" type="text/css" />
+
+<style type="text/css">
+.log { display: none;} 
+.stack-trace { display: none;} 
+</style>
+<script type="text/javascript">
+<!--
+function flip(e) {
+  current = e.style.display;
+  if (current == 'block') {
+    e.style.display = 'none';
+    return 0;
+  }
+  else {
+    e.style.display = 'block';
+    return 1;
+  }
+}
+
+function toggleBox(szDivId, elem, msg1, msg2)
+{
+  var res = -1;  if (document.getElementById) {
+    res = flip(document.getElementById(szDivId));
+  }
+  else if (document.all) {
+    // this is the way old msie versions work
+    res = flip(document.all[szDivId]);
+  }
+  if(elem) {
+    if(res == 0) elem.innerHTML = msg1; else elem.innerHTML = msg2;
+  }
+
+}
+
+function toggleAllBoxes() {
+  if (document.getElementsByTagName) {
+    d = document.getElementsByTagName('div');
+    for (i = 0; i < d.length; i++) {
+      if (d[i].className == 'log') {
+        flip(d[i]);
+      }
+    }
+  }
+}
+
+// -->
+</script>
+
+</head>
+<body>
+<h2 align='center'>Default test</h2><table border='1' align="center">
+<tr>
+<td>Tests passed/Failed/Skipped:</td><td>2/0/0</td>
+</tr><tr>
+<td>Started on:</td><td>Wed Jul 04 12:17:19 CEST 2012</td>
+</tr>
+<tr><td>Total time:</td><td>0 seconds (429 ms)</td>
+</tr><tr>
+<td>Included groups:</td><td></td>
+</tr><tr>
+<td>Excluded groups:</td><td></td>
+</tr>
+</table><p/>
+<small><i>(Hover the method name to see the test class name)</i></small><p/>
+<table width='100%' border='1' class='invocation-passed'>
+<tr><td colspan='4' align='center'><b>PASSED TESTS</b></td></tr>
+<tr><td><b>Test method</b></td>
+<td width="30%"><b>Exception</b></td>
+<td width="10%"><b>Time (seconds)</b></td>
+<td><b>Instance</b></td>
+</tr>
+<tr>
+<td title='org.apache.ace.client.rest.RESTClientTest.testPathTransforms()'><b>testPathTransforms</b><br>Test class: org.apache.ace.client.rest.RESTClientTest</td>
+<td></td>
+<td>0</td>
+<td>org.apache.ace.client.rest.RESTClientTest@44050988</td></tr>
+<tr>
+<td title='org.apache.ace.client.rest.RESTClientTest.testPropertyGetter()'><b>testPropertyGetter</b><br>Test class: org.apache.ace.client.rest.RESTClientTest</td>
+<td></td>
+<td>0</td>
+<td>org.apache.ace.client.rest.RESTClientTest@44050988</td></tr>
+</table><p>
+</body>
+</html>
\ No newline at end of file

Added: ace/sandbox/marrs/org.apache.ace.client.rest/test-output/Default suite/Default test.xml
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.rest/test-output/Default%20suite/Default%20test.xml?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.rest/test-output/Default suite/Default test.xml (added)
+++ ace/sandbox/marrs/org.apache.ace.client.rest/test-output/Default suite/Default test.xml Thu Jul  5 12:09:30 2012
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated by org.testng.reporters.JUnitXMLReporter -->
+<testsuite hostname="Marcels-MacBook-Pro.local" name="Default test" tests="2" failures="0" timestamp="4 Jul 2012 10:17:20 GMT" time="0.429" errors="0">
+  <testcase name="testPathTransforms" time="0.414" classname="org.apache.ace.client.rest.RESTClientTest"/>
+  <testcase name="testPropertyGetter" time="0.0010" classname="org.apache.ace.client.rest.RESTClientTest"/>
+</testsuite> <!-- Default test -->

Added: ace/sandbox/marrs/org.apache.ace.client.rest/test-output/bullet_point.png
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.rest/test-output/bullet_point.png?rev=1357570&view=auto
==============================================================================
Binary file - no diff available.

Propchange: ace/sandbox/marrs/org.apache.ace.client.rest/test-output/bullet_point.png
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream

Added: ace/sandbox/marrs/org.apache.ace.client.rest/test-output/collapseall.gif
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.rest/test-output/collapseall.gif?rev=1357570&view=auto
==============================================================================
Binary file - no diff available.

Propchange: ace/sandbox/marrs/org.apache.ace.client.rest/test-output/collapseall.gif
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream

Added: ace/sandbox/marrs/org.apache.ace.client.rest/test-output/emailable-report.html
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.rest/test-output/emailable-report.html?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.rest/test-output/emailable-report.html (added)
+++ ace/sandbox/marrs/org.apache.ace.client.rest/test-output/emailable-report.html Thu Jul  5 12:09:30 2012
@@ -0,0 +1,45 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>TestNG:  Unit Test</title>
+<style type="text/css">
+table caption,table.info_table,table.param,table.passed,table.failed {margin-bottom:10px;border:1px solid #000099;border-collapse:collapse;empty-cells:show;}
+table.info_table td,table.info_table th,table.param td,table.param th,table.passed td,table.passed th,table.failed td,table.failed th {
+border:1px solid #000099;padding:.25em .5em .25em .5em
+}
+table.param th {vertical-align:bottom}
+td.numi,th.numi,td.numi_attn {
+text-align:right
+}
+tr.total td {font-weight:bold}
+table caption {
+text-align:center;font-weight:bold;
+}
+table.passed tr.stripe td,table tr.passedodd td {background-color: #00AA00;}
+table.passed td,table tr.passedeven td {background-color: #33FF33;}
+table.passed tr.stripe td,table tr.skippedodd td {background-color: #cccccc;}
+table.passed td,table tr.skippedodd td {background-color: #dddddd;}
+table.failed tr.stripe td,table tr.failedodd td,table.param td.numi_attn {background-color: #FF3333;}
+table.failed td,table tr.failedeven td,table.param tr.stripe td.numi_attn {background-color: #DD0000;}
+tr.stripe td,tr.stripe th {background-color: #E6EBF9;}
+p.totop {font-size:85%;text-align:center;border-bottom:2px black solid}
+div.shootout {padding:2em;border:3px #4854A8 solid}
+</style>
+</head>
+<body>
+<table cellspacing=0 cellpadding=0 class="param">
+<tr><th>Test</th><th class="numi">Methods<br/>Passed</th><th class="numi">Scenarios<br/>Passed</th><th class="numi"># skipped</th><th class="numi"># failed</th><th class="numi">Total<br/>Time</th><th class="numi">Included<br/>Groups</th><th class="numi">Excluded<br/>Groups</th></tr>
+<tr><td style="text-align:left;padding-right:2em">Default test</td><td class="numi">2</td><td class="numi">2</td><td class="numi">0</td><td class="numi">0</td><td class="numi">0.4 seconds</td><td class="numi"></td><td class="numi"></td></tr>
+</table>
+<a id="summary"></a>
+<table cellspacing=0 cellpadding=0 class="passed">
+<tr><th>Class</th><th>Method</th><th># of<br/>Scenarios</th><th>Start</th><th>Time<br/>(ms)</th></tr>
+<tr><th colspan="4">Default test &#8212; passed</th></tr>
+<tr class="passedodd"><td rowspan="2">org.apache.ace.client.rest.RESTClientTest<td><a href="#m1"><b>testPathTransforms</b> (unit) </a></td><td class="numi">1</td><td>1341397039609</td><td class="numi">414</td></tr><tr class="passedodd"><td><a href="#m2"><b>testPropertyGetter</b> (unit) </a></td><td class="numi">1</td><td>1341397040024</td><td class="numi">1</td></tr>
+</table>
+<h1>Default test</h1>
+<a id="m1"></a><h2>org.apache.ace.client.rest.RESTClientTest:testPathTransforms</h2>
+<p class="totop"><a href="#summary">back to summary</a></p>
+<a id="m2"></a><h2>org.apache.ace.client.rest.RESTClientTest:testPropertyGetter</h2>
+<p class="totop"><a href="#summary">back to summary</a></p>
+</body></html>

Added: ace/sandbox/marrs/org.apache.ace.client.rest/test-output/failed.png
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.rest/test-output/failed.png?rev=1357570&view=auto
==============================================================================
Binary file - no diff available.

Propchange: ace/sandbox/marrs/org.apache.ace.client.rest/test-output/failed.png
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream

Added: ace/sandbox/marrs/org.apache.ace.client.rest/test-output/index.html
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.rest/test-output/index.html?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.rest/test-output/index.html (added)
+++ ace/sandbox/marrs/org.apache.ace.client.rest/test-output/index.html Thu Jul  5 12:09:30 2012
@@ -0,0 +1,263 @@
+<!DOCTYPE html>
+
+<html>
+  <head>
+  <title>TestNG new reports</title>
+
+    <link type="text/css" href="testng-reports.css" rel="stylesheet" />  
+    <script type="text/javascript" src="jquery-1.7.1.min.js"></script>
+    <script type="text/javascript" src="testng-reports.js"></script>
+    <script type="text/javascript" src="https://www.google.com/jsapi"></script>
+    <script type='text/javascript'>
+      google.load('visualization', '1', {packages:['table']});
+      google.setOnLoadCallback(drawTable);
+      var suiteTableInitFunctions = new Array();
+      var suiteTableData = new Array();
+    </script>
+    <!--
+      <script type="text/javascript" src="jquery-ui/js/jquery-ui-1.8.16.custom.min.js"></script>
+     -->
+  </head>
+
+  <body>
+    <div class="top-banner-root">
+      <span class="top-banner-title-font">Test results</span>
+      <br/>
+      <span class="top-banner-font-1">1 suite</span>
+    </div> <!-- top-banner-root -->
+    <div class="navigator-root">
+      <div class="navigator-suite-header">
+        <span>All suites</span>
+        <a href="#" class="collapse-all-link" title="Collapse/expand all the suites">
+          <img class="collapse-all-icon" src="collapseall.gif">
+          </img> <!-- collapse-all-icon -->
+        </a> <!-- collapse-all-link -->
+      </div> <!-- navigator-suite-header -->
+      <div class="suite">
+        <div class="rounded-window">
+          <div class="suite-header light-rounded-window-top">
+            <a href="#" class="navigator-link" panel-name="suite-Default_suite">
+              <span class="suite-name border-passed">Default suite</span>
+            </a> <!-- navigator-link -->
+          </div> <!-- suite-header light-rounded-window-top -->
+          <div class="navigator-suite-content">
+            <div class="suite-section-title">
+              <span>Info</span>
+            </div> <!-- suite-section-title -->
+            <div class="suite-section-content">
+              <ul>
+                <li>
+                  <a href="#" class="navigator-link " panel-name="test-xml-Default_suite">
+                    <span>testng-customsuite.xml</span>
+                  </a> <!-- navigator-link  -->
+                </li>
+                <li>
+                  <a href="#" class="navigator-link " panel-name="testlist-Default_suite">
+                    <span class="test-stats">1 test</span>
+                  </a> <!-- navigator-link  -->
+                </li>
+                <li>
+                  <a href="#" class="navigator-link " panel-name="group-Default_suite">
+                    <span>1 group</span>
+                  </a> <!-- navigator-link  -->
+                </li>
+                <li>
+                  <a href="#" class="navigator-link " panel-name="times-Default_suite">
+                    <span>Times</span>
+                  </a> <!-- navigator-link  -->
+                </li>
+                <li>
+                  <a href="#" class="navigator-link " panel-name="reporter-Default_suite">
+                    <span>Reporter output</span>
+                  </a> <!-- navigator-link  -->
+                </li>
+                <li>
+                  <a href="#" class="navigator-link " panel-name="ignored-methods-Default_suite">
+                    <span>Ignored methods</span>
+                  </a> <!-- navigator-link  -->
+                </li>
+                <li>
+                  <a href="#" class="navigator-link " panel-name="chronological-Default_suite">
+                    <span>Chronological view</span>
+                  </a> <!-- navigator-link  -->
+                </li>
+              </ul>
+            </div> <!-- suite-section-content -->
+            <div class="result-section">
+              <div class="suite-section-title">
+                <span>Results</span>
+              </div> <!-- suite-section-title -->
+              <div class="suite-section-content">
+                <ul>
+                  <li>
+                    <span class="method-stats">2 methods,   2 passed</span>
+                  </li>
+                  <li>
+                    <span class="method-list-title passed">Passed methods</span>
+                    <span class="show-or-hide-methods passed">
+                      <a href="#" panel-name="suite-Default_suite" class="hide-methods passed suite-Default_suite"> (hide)</a> <!-- hide-methods passed suite-Default_suite -->
+                      <a href="#" panel-name="suite-Default_suite" class="show-methods passed suite-Default_suite"> (show)</a> <!-- show-methods passed suite-Default_suite -->
+                    </span>
+                    <div class="method-list-content passed suite-Default_suite">
+                      <span>
+                        <img width="3%" src="passed.png"/>
+                        <a href="#" class="method navigator-link" panel-name="suite-Default_suite" title="org.apache.ace.client.rest.RESTClientTest" hash-for-method="testPathTransforms">testPathTransforms</a> <!-- method navigator-link -->
+                      </span>
+                      <br/>
+                      <span>
+                        <img width="3%" src="passed.png"/>
+                        <a href="#" class="method navigator-link" panel-name="suite-Default_suite" title="org.apache.ace.client.rest.RESTClientTest" hash-for-method="testPropertyGetter">testPropertyGetter</a> <!-- method navigator-link -->
+                      </span>
+                      <br/>
+                    </div> <!-- method-list-content passed suite-Default_suite -->
+                  </li>
+                </ul>
+              </div> <!-- suite-section-content -->
+            </div> <!-- result-section -->
+          </div> <!-- navigator-suite-content -->
+        </div> <!-- rounded-window -->
+      </div> <!-- suite -->
+    </div> <!-- navigator-root -->
+    <div class="wrapper">
+      <div class="main-panel-root">
+        <div panel-name="suite-Default_suite" class="panel Default_suite">
+          <div class="suite-Default_suite-class-passed">
+            <div class="main-panel-header rounded-window-top">
+              <img src="passed.png"/>
+              <span class="class-name">org.apache.ace.client.rest.RESTClientTest</span>
+            </div> <!-- main-panel-header rounded-window-top -->
+            <div class="main-panel-content rounded-window-bottom">
+              <div class="method">
+                <div class="method-content">
+                  <a name="testPathTransforms">
+                  </a> <!-- testPathTransforms -->
+                  <span class="method-name">testPathTransforms</span>
+                </div> <!-- method-content -->
+              </div> <!-- method -->
+              <div class="method">
+                <div class="method-content">
+                  <a name="testPropertyGetter">
+                  </a> <!-- testPropertyGetter -->
+                  <span class="method-name">testPropertyGetter</span>
+                </div> <!-- method-content -->
+              </div> <!-- method -->
+            </div> <!-- main-panel-content rounded-window-bottom -->
+          </div> <!-- suite-Default_suite-class-passed -->
+        </div> <!-- panel Default_suite -->
+        <div panel-name="test-xml-Default_suite" class="panel">
+          <div class="main-panel-header rounded-window-top">
+            <span class="header-content">/private/var/folders/4k/58plh09n605ffjkhv0kpcx540000gn/T/testng-eclipse-599445437/testng-customsuite.xml</span>
+          </div> <!-- main-panel-header rounded-window-top -->
+          <div class="main-panel-content rounded-window-bottom">
+            <pre>
+&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+&lt;!DOCTYPE suite SYSTEM &quot;http://testng.org/testng-1.0.dtd&quot;&gt;
+&lt;suite name=&quot;Default suite&quot;&gt;
+  &lt;test verbose=&quot;2&quot; name=&quot;Default test&quot; preserve-order=&quot;true&quot;&gt;
+    &lt;classes&gt;
+      &lt;class name=&quot;org.apache.ace.client.rest.RESTClientTest&quot;/&gt;
+    &lt;/classes&gt;
+  &lt;/test&gt; &lt;!-- Default test --&gt;
+&lt;/suite&gt; &lt;!-- Default suite --&gt;
+            </pre>
+          </div> <!-- main-panel-content rounded-window-bottom -->
+        </div> <!-- panel -->
+        <div panel-name="testlist-Default_suite" class="panel">
+          <div class="main-panel-header rounded-window-top">
+            <span class="header-content">Tests for Default suite</span>
+          </div> <!-- main-panel-header rounded-window-top -->
+          <div class="main-panel-content rounded-window-bottom">
+            <ul>
+              <li>
+                <span class="test-name">Default test (1 class)</span>
+              </li>
+            </ul>
+          </div> <!-- main-panel-content rounded-window-bottom -->
+        </div> <!-- panel -->
+        <div panel-name="group-Default_suite" class="panel">
+          <div class="main-panel-header rounded-window-top">
+            <span class="header-content">Groups for Default suite</span>
+          </div> <!-- main-panel-header rounded-window-top -->
+          <div class="main-panel-content rounded-window-bottom">
+            <div class="test-group">
+              <span class="test-group-name">unit</span>
+              <br/>
+              <div class="method-in-group">
+                <span class="method-in-group-name">testPathTransforms</span>
+                <br/>
+              </div> <!-- method-in-group -->
+              <div class="method-in-group">
+                <span class="method-in-group-name">testPropertyGetter</span>
+                <br/>
+              </div> <!-- method-in-group -->
+            </div> <!-- test-group -->
+          </div> <!-- main-panel-content rounded-window-bottom -->
+        </div> <!-- panel -->
+        <div panel-name="times-Default_suite" class="panel">
+          <div class="main-panel-header rounded-window-top">
+            <span class="header-content">Times for Default suite</span>
+          </div> <!-- main-panel-header rounded-window-top -->
+          <div class="main-panel-content rounded-window-bottom">
+            <div class="times-div">
+              <script type="text/javascript">
+suiteTableInitFunctions.push('tableData_Default_suite');
+function tableData_Default_suite() {
+var data = new google.visualization.DataTable();
+data.addColumn('number', 'Number');
+data.addColumn('string', 'Method');
+data.addColumn('string', 'Class');
+data.addColumn('number', 'Time (ms)');
+data.addRows(2);
+data.setCell(0, 0, 0)
+data.setCell(0, 1, 'testPathTransforms')
+data.setCell(0, 2, 'org.apache.ace.client.rest.RESTClientTest')
+data.setCell(0, 3, 414);
+data.setCell(1, 0, 1)
+data.setCell(1, 1, 'testPropertyGetter')
+data.setCell(1, 2, 'org.apache.ace.client.rest.RESTClientTest')
+data.setCell(1, 3, 1);
+window.suiteTableData['Default_suite']= { tableData: data, tableDiv: 'times-div-Default_suite'}
+return data;
+}
+              </script>
+              <span class="suite-total-time">Total running time: 415 ms</span>
+              <div id="times-div-Default_suite">
+              </div> <!-- times-div-Default_suite -->
+            </div> <!-- times-div -->
+          </div> <!-- main-panel-content rounded-window-bottom -->
+        </div> <!-- panel -->
+        <div panel-name="reporter-Default_suite" class="panel">
+          <div class="main-panel-header rounded-window-top">
+            <span class="header-content">Reporter output for Default suite</span>
+          </div> <!-- main-panel-header rounded-window-top -->
+          <div class="main-panel-content rounded-window-bottom">
+          </div> <!-- main-panel-content rounded-window-bottom -->
+        </div> <!-- panel -->
+        <div panel-name="ignored-methods-Default_suite" class="panel">
+          <div class="main-panel-header rounded-window-top">
+            <span class="header-content">0 ignored methods</span>
+          </div> <!-- main-panel-header rounded-window-top -->
+          <div class="main-panel-content rounded-window-bottom">
+          </div> <!-- main-panel-content rounded-window-bottom -->
+        </div> <!-- panel -->
+        <div panel-name="chronological-Default_suite" class="panel">
+          <div class="main-panel-header rounded-window-top">
+            <span class="header-content">Methods in chronological order</span>
+          </div> <!-- main-panel-header rounded-window-top -->
+          <div class="main-panel-content rounded-window-bottom">
+            <div class="chronological-class">
+              <div class="chronological-class-name">org.apache.ace.client.rest.RESTClientTest</div> <!-- chronological-class-name -->
+              <div class="test-method">
+                <span class="method-name">testPathTransforms</span>
+                <span class="method-start">0 ms</span>
+              </div> <!-- test-method -->
+              <div class="test-method">
+                <span class="method-name">testPropertyGetter</span>
+                <span class="method-start">415 ms</span>
+              </div> <!-- test-method -->
+          </div> <!-- main-panel-content rounded-window-bottom -->
+        </div> <!-- panel -->
+      </div> <!-- main-panel-root -->
+    </div> <!-- wrapper -->
+  </body>
+</html>