/******************************************************************************* * Copyright (c) 2010, 2015 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.orion.server.servlets; import java.io.IOException; import java.util.Iterator; import java.util.Map; import java.util.Set; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.orion.internal.server.servlets.Activator; import org.eclipse.orion.server.core.EncodingUtils; import org.eclipse.orion.server.core.OrionConfiguration; import org.eclipse.orion.server.core.metastore.IMetaStore; import org.eclipse.orion.server.core.metastore.MetadataInfo; import org.eclipse.orion.server.core.metastore.ProjectInfo; import org.eclipse.orion.server.core.metastore.UserInfo; import org.eclipse.orion.server.core.metastore.WorkspaceInfo; import org.eclipse.osgi.util.NLS; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; /** * A servlet for accessing and modifying preferences. * GET /prefs/ to return the preferences and children of the preference root node as a JSON object (the children of the root are the scopes) * GET /prefs/[path] returns the preferences and children of the given preference node as a JSON object * GET /prefs/[path]?key=[key] returns the value of the preference in the node at the given path, with the given key, as a JSON string * PUT /prefs/[path] sets all the preferences at the given path to the provided JSON object * PUT /prefs/[path]?key=[key]&value=[value] sets the value of the preference at the given path with the given key to the provided value * DELETE /prefs/[path] to delete an entire preference node * DELETE /prefs/[path]?key=[key] to delete a single preference at the given path with the given key */ public class PreferencesServlet extends OrionServlet { private static final long serialVersionUID = 1L; public PreferencesServlet() { super(); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { traceRequest(req); // ensure that there is at least one additional segment following // the /user, /workspace or /project segment IPath path = getPath(req); if (path.segmentCount() < 2) { handleNotFound(req, resp, HttpServletResponse.SC_METHOD_NOT_ALLOWED); return; } MetadataInfo node = getNode(path, req, resp); if (node == null) { handleNotFound(req, resp, HttpServletResponse.SC_NOT_FOUND); return; } String key = req.getParameter("key"); //$NON-NLS-1$ try { String prefix = getPrefix(path); //if a key is specified get that single value, otherwise get the entire node JSONObject result = null; if (key != null) { prefix = prefix + '/' + key; String value = node.getProperty(prefix.toString()); if (value == null) { handleNotFound(req, resp, HttpServletResponse.SC_NOT_FOUND); return; } result = new JSONObject().put(key, value); } else { result = toJSON(req, prefix.toString(), node); //empty result should be treated as not found if (result.length() == 0) { //the preference is not found, return an empty JSON object. result = new JSONObject(); } } writeJSONResponse(req, resp, result); } catch (Exception e) { handleException(resp, NLS.bind("Failed to retrieve preferences for path {0}", req.getPathInfo()), e); return; } } private IPath getPath(HttpServletRequest req) { String pathString = req.getPathInfo(); if (pathString == null) { pathString = ""; //$NON-NLS-1$ } return new Path(pathString); } /** * Returns the prefix for the preference to be retrieved or manipulated. */ private String getPrefix(IPath path) { String scope = path.segment(0); if ("user".equalsIgnoreCase(scope)) { //$NON-NLS-1$ //format is /user/prefix path = path.removeFirstSegments(1); } else if ("workspace".equalsIgnoreCase(scope)) { //$NON-NLS-1$ //format is /workspace/{workspaceId}/prefix path = path.removeFirstSegments(2); } else if ("project".equalsIgnoreCase(scope)) { //$NON-NLS-1$ //format is /project/{workspaceId}/{projectName}/prefix path = path.removeFirstSegments(3); } return path.toString(); } @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { traceRequest(req); // ensure that there is at least one additional segment following // the /user, /workspace or /project segment IPath path = getPath(req); if (path.segmentCount() < 2) { handleNotFound(req, resp, HttpServletResponse.SC_METHOD_NOT_ALLOWED); return; } MetadataInfo info = getNode(path, req, resp); if (info == null) { //should not fail on delete when resource doesn't exist resp.setStatus(HttpServletResponse.SC_NO_CONTENT); return; } String key = req.getParameter("key"); try { String prefix = getPrefix(path); //if a key is specified write that single value, otherwise write the entire node boolean changed = false; if (key != null) { prefix = prefix + '/' + key; changed = info.setProperty(prefix.toString(), null) != null; } else { //can't overwrite base user settings via preference servlet if (prefix.startsWith("User")) { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); return; } changed = removeMatchingProperties(info, prefix.toString()); } if (changed) save(info); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } catch (Exception e) { handleException(resp, NLS.bind("Failed to retrieve preferences for path {0}", req.getPathInfo()), e); return; } } /** * Returns the metadata object associated with this request. This method controls * exactly what metadata objects are exposed via this service. If there is no matching * metadata object for the request, this method handles the appropriate response * and returns <code>null</code>. * @param req * @param resp */ private MetadataInfo getNode(IPath path, HttpServletRequest req, HttpServletResponse resp) throws ServletException { int segmentCount = path.segmentCount(); String scope = path.segment(0); try { if ("user".equalsIgnoreCase(scope)) { //$NON-NLS-1$ String username = req.getRemoteUser(); if (username == null) { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); return null; } return OrionConfiguration.getMetaStore().readUser(username); } else if ("workspace".equalsIgnoreCase(scope) && segmentCount > 1) { //$NON-NLS-1$ //format is /workspace/{workspaceId} return OrionConfiguration.getMetaStore().readWorkspace(path.segment(1)); } else if ("project".equalsIgnoreCase(scope) && segmentCount > 2) { //$NON-NLS-1$ //format is /project/{workspaceId}/{projectName} return OrionConfiguration.getMetaStore().readProject(path.segment(1), path.segment(2)); } } catch (CoreException e) { handleException(resp, "Internal error obtaining preferences", e, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return null; } //invalid prefix handleNotFound(req, resp, HttpServletResponse.SC_METHOD_NOT_ALLOWED); return null; } private void handleNotFound(HttpServletRequest req, HttpServletResponse resp, int code) throws ServletException { String path = req.getPathInfo() == null ? "/" : req.getPathInfo(); String msg = code == HttpServletResponse.SC_NOT_FOUND ? "No preferences found for path {0}" : "Invalid preference path {0}"; handleException(resp, new Status(IStatus.ERROR, Activator.PI_SERVER_SERVLETS, NLS.bind(msg, EncodingUtils.encodeForHTML(path.toString()))), code); } @SuppressWarnings("unchecked") @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { traceRequest(req); // ensure that there is at least one additional segment following // the /user, /workspace or /project segment IPath path = getPath(req); if (path.segmentCount() < 2) { handleNotFound(req, resp, HttpServletResponse.SC_METHOD_NOT_ALLOWED); return; } MetadataInfo info = getNode(path, req, resp); if (info == null) { handleNotFound(req, resp, HttpServletResponse.SC_NOT_FOUND); return; } String key = req.getParameter("key"); //$NON-NLS-1$ String prefix = getPrefix(path); try { boolean changed = false; if (key != null) { prefix = prefix + '/' + key; String newValue = req.getParameter("value"); //$NON-NLS-1$ String oldValue = info.setProperty(prefix.toString(), newValue); changed = !newValue.equals(oldValue); } else { JSONObject newNode = new JSONObject(new JSONTokener(req.getReader())); //can't overwrite base user settings via preference servlet if (prefix.startsWith("User")) { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); return; } //operations should not be removed by PUT if (!prefix.equals("operations")) { //clear existing values matching prefix changed |= removeMatchingProperties(info, prefix.toString()); } for (Iterator<String> it = newNode.keys(); it.hasNext();) { key = it.next(); String newValue = newNode.getString(key); String qualifiedKey = prefix + '/' + key; String oldValue = info.setProperty(qualifiedKey, newValue); changed |= !newValue.equals(oldValue); } } if (changed) save(info); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } catch (Exception e) { handleException(resp, NLS.bind("Failed to store preferences for {0}", req.getRequestURL()), e); return; } } private void save(MetadataInfo info) throws CoreException { IMetaStore store = OrionConfiguration.getMetaStore(); if (info instanceof UserInfo) { store.updateUser((UserInfo) info); } else if (info instanceof WorkspaceInfo) { store.updateWorkspace((WorkspaceInfo) info); } else if (info instanceof ProjectInfo) { store.updateProject((ProjectInfo) info); } } private boolean removeMatchingProperties(MetadataInfo info, String prefix) { final Map<String, String> properties = info.getProperties(); boolean changed = false; final Set<String> keySet = properties.keySet(); //convert keys to array to avoid concurrent modification of set for (String key : keySet.toArray(new String[keySet.size()])) { if (key.startsWith(prefix)) { String previous = info.setProperty(key, null); if (previous != null) changed = true; } } return changed; } /** * Serializes a preference node as a JSON object. */ private JSONObject toJSON(HttpServletRequest req, String prefix, MetadataInfo info) throws JSONException { JSONObject result = new JSONObject(); final Map<String, String> properties = info.getProperties(); //from client's perspective key is the part after prefix for (String key : properties.keySet()) { if (key.startsWith(prefix) && !key.startsWith("User")) result.put(key.substring(prefix.length() + 1), stringToJSON(properties.get(key))); } return result; } /** * Converts a string representation of a JSON value into the appropriate object type. * Possible return types are: String, JSONObject, JSONArray, Boolean, Integer, Long, Double */ private static Object stringToJSON(String input) { if (input == null) return null; Object result = null; //test if the value is a JSON object if (input.startsWith("{")) { //$NON-NLS-1$ try { result = new JSONObject(input); } catch (JSONException e) { //treat as string } } else if (input.startsWith("[")) { //$NON-NLS-1$ try { result = new JSONArray(input); } catch (JSONException e) { //treat as string } } if (result == null) result = JSONObject.stringToValue(input); return result; } }