blob: 0d0e1b49e220903cf349a5b4f42fe255fd1168db [file] [log] [blame]
/*
* 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.HashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.apache.ace.client.repository.RepositoryObject;
import org.apache.ace.client.repository.stateful.StatefulTargetObject;
import org.apache.ace.client.workspace.Workspace;
import org.apache.ace.client.workspace.WorkspaceManager;
import org.apache.ace.feedback.Event;
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, HttpSessionListener {
private static final String SESSION_KEY_WORKSPACES = "workspaces";
/** Timeout in seconds for REST sessions. */
private static final String KEY_SESSION_TIMEOUT = "session.timeout";
private static final int DEFAULT_SESSION_TIMEOUT = 300; // in seconds.
/** 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";
/** 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 volatile LogService m_logger;
private volatile WorkspaceManager m_workspaceManager;
private volatile int m_sessionTimeout = DEFAULT_SESSION_TIMEOUT;
private final Gson m_gson;
/**
* Creates a new {@link RESTClientServlet} instance.
*/
public RESTClientServlet() {
m_gson = (new GsonBuilder())
.registerTypeHierarchyAdapter(RepositoryObject.class, new RepositoryObjectSerializer())
.registerTypeHierarchyAdapter(Event.class, new LogEventSerializer())
.create();
}
/**
* 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;
}
/**
* @see javax.servlet.http.HttpServlet#doDelete(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
@SuppressWarnings("unchecked")
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = getSession(req);
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 = m_workspaceManager.getWorkspace(id);
if (workspace == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Could not find workspace: " + id);
return;
}
if (pathElements.length == 2) {
try {
Set<String> workspaces = (Set<String>) session.getAttribute(SESSION_KEY_WORKSPACES);
if (workspaces != null) {
workspaces.remove(workspace.getSessionID());
session.setAttribute(SESSION_KEY_WORKSPACES, workspaces);
}
m_workspaceManager.removeWorkspace(id);
}
catch (IOException ioe) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Could not delete work area: " + ioe.getMessage());
}
}
else if (pathElements.length == 4) {
deleteRepositoryObject(workspace, pathElements[2], pathElements[3], resp);
}
else {
// All other path lengths...
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
private HttpSession getSession(HttpServletRequest req) {
HttpSession session = req.getSession(false);
if (session == null) {
session = req.getSession(true);
session.setMaxInactiveInterval(m_sessionTimeout); // seconds
}
return session;
}
/**
* @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 {
HttpSession session = getSession(req);
if (session == null) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
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 = m_workspaceManager.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
@SuppressWarnings("unchecked")
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = getSession(req);
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) {
Workspace workspace = m_workspaceManager.createWorkspace(req.getParameterMap(), req);
if (workspace == null) {
resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
else {
Set<String> workspaces = (Set<String>) session.getAttribute(SESSION_KEY_WORKSPACES);
if (workspaces == null) {
workspaces = new HashSet<>();
}
workspaces.add(workspace.getSessionID());
session.setAttribute(SESSION_KEY_WORKSPACES, workspaces);
resp.sendRedirect(req.getServletPath() + "/" + buildPathFromElements(WORK_FOLDER, workspace.getSessionID()));
}
}
else {
// more than one path elements...
Workspace workspace = m_workspaceManager.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, req, 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 {
HttpSession session = getSession(req);
if (session == null) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
String[] pathElements = getPathElements(req);
if (pathElements == null || pathElements.length != 4 || !WORK_FOLDER.equals(pathElements[0])) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
Workspace workspace = m_workspaceManager.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, req, 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, HttpServletRequest req, HttpServletResponse resp) throws IOException {
try {
RepositoryObject object = workspace.createRepositoryObject(entityType, data.attributes, data.tags);
resp.sendRedirect(req.getServletPath() + "/" + buildPathFromElements(WORK_FOLDER, workspace.getSessionID(), entityType, object.getDefinition()));
}
catch (IllegalArgumentException e) {
m_logger.log(LogService.LOG_WARNING, "Failed to add entity of type: " + entityType + " with data: " + data);
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Could not add entity of type " + entityType + " with data: " + data);
}
}
/**
* 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);
}
}
/**
* 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<Event> 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<Event> 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));
}
}
/**
* 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, HttpServletRequest req, HttpServletResponse resp) throws IOException {
try {
workspace.updateRepositoryObject(entityType, entityId, data.attributes, data.tags);
resp.sendRedirect(req.getServletPath() + "/" + buildPathFromElements(WORK_FOLDER, workspace.getSessionID(), entityType, entityId));
}
catch (IllegalArgumentException e) {
m_logger.log(LogService.LOG_WARNING, "Failed to update entity of type: " + entityType + " with data: " + data, e);
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Could not update entity of type " + entityType + " with data: " + data);
}
}
/**
* 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
}
}
@Override
public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
if (properties == null) {
// defaults
m_sessionTimeout = DEFAULT_SESSION_TIMEOUT;
}
else {
try {
Object timeoutObject = properties.get(KEY_SESSION_TIMEOUT);
if (timeoutObject != null) {
if (timeoutObject instanceof Integer) {
m_sessionTimeout = (Integer) timeoutObject;
}
else {
m_sessionTimeout = Integer.parseInt(timeoutObject.toString());
}
if (m_sessionTimeout < 1) {
m_sessionTimeout = DEFAULT_SESSION_TIMEOUT;
throw new ConfigurationException(KEY_SESSION_TIMEOUT, "Session timeout should be at least 1 second");
}
}
}
catch (Exception e) {
throw new ConfigurationException(KEY_SESSION_TIMEOUT, "Could not parse timeout, it should either be a string or integer");
}
}
}
@Override
public void sessionCreated(HttpSessionEvent e) {
}
@Override
@SuppressWarnings("unchecked")
public void sessionDestroyed(HttpSessionEvent e) {
HttpSession session = e.getSession();
if (session != null) {
Set<String> workspaces = (Set<String>) session.getAttribute(SESSION_KEY_WORKSPACES);
if (workspaces != null) {
for (String id : workspaces) {
try {
m_workspaceManager.removeWorkspace(id);
}
catch (IOException ioe) {
m_logger.log(LogService.LOG_WARNING, "Error while removing workspace after session timeout", ioe);
}
}
}
}
}
}