/* * #! * Ontopia TMRAP * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * Licensed 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 net.ontopia.topicmaps.utils.tmrap; import java.io.IOException; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.zip.GZIPOutputStream; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.ontopia.infoset.core.LocatorIF; import net.ontopia.infoset.impl.basic.URILocator; import net.ontopia.topicmaps.core.TopicIF; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.topicmaps.nav2.core.NavigatorApplicationIF; import net.ontopia.topicmaps.nav2.core.NavigatorRuntimeException; import net.ontopia.topicmaps.nav2.utils.NavigatorUtils; import net.ontopia.topicmaps.xml.XTMTopicMapWriter; import net.ontopia.xml.PrettyPrinter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * EXPERIMENTAL: Implements the TMRAP protocol. */ public class RAPServlet extends HttpServlet { // Much wanted by Serializable. (The number is randomly typed). private static final long serialVersionUID = 3585458045457498992l; // initialization of logging facility private static Logger log = LoggerFactory.getLogger(RAPServlet.class.getName()); // Static names for request parameters public static final String CLIENT_PARAMETER_NAME = "client"; public static final String FRAGMENT_PARAMETER_NAME = "fragment"; public static final String INDICATOR_PARAMETER_NAME = "identifier"; public static final String SOURCE_PARAMETER_NAME = "item"; public static final String SUBJECT_PARAMETER_NAME = "subject"; public static final String SYNTAX_PARAMETER_NAME = "syntax"; public static final String TOLOG_PARAMETER_NAME = "tolog"; public static final String TOPICMAP_PARAMETER_NAME = "topicmap"; public static final String VIEW_PARAMETER_NAME = "view"; public static final String COMPRESS_PARAMETER_NAME = "compress"; public static final String STATEMENT_PARAMETER_NAME = "tolog"; public static final String SYNTAX_ASTMA = "text/x-astma"; public static final String SYNTAX_LTM = "text/x-ltm"; public static final String SYNTAX_TM_XML = "text/x-tmxml"; public static final String SYNTAX_TOLOG = "text/x-tolog"; public static final String SYNTAX_CTM = "text/x-ctm"; public static final String SYNTAX_XTM = "application/x-xtm"; public static final String RAP_NAMESPACE = "http://psi.ontopia.net/tmrap/"; // Used to register type listeners Map<TopicIF, Map<String, String>> clientListeners = new HashMap<TopicIF, Map<String, String>>(); private TMRAPConfiguration rapconfig; // --- Servlet interface implementation public void init(ServletConfig config) throws ServletException { super.init(config); rapconfig = new TMRAPConfiguration(config); } /** * Supported TMRAP protocol requests: * <pre> * GET /xtm-fragment?topicmap=[]&source=[]&indicator=[] * GET /topic-page?topicmap=[]&source=[]&indicator=[] * </pre> */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { doGet(request, response, request.getRequestURL().toString()); } /** INTERNAL * A variant of 'doGet' that allows the caller to specify the URLString. * Useful when 'request' doesn't support getRequestURL() (e.g. when testing). */ public void doGet(HttpServletRequest request, HttpServletResponse response, String URLString) throws IOException, ServletException { if (URLString.endsWith("get-tolog")) getTolog(request, response); else if (URLString.endsWith("get-topic")) getTopic(request, response); else if (URLString.endsWith("get-topic-page")) getTopicPage(request, response); else reportError(response, "No such GET request: " + URLString); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { doPost(request, response, request.getRequestURL().toString()); } public void doPost(HttpServletRequest request, HttpServletResponse response, String URLString) throws IOException { if (URLString.endsWith("add-fragment")) addFragment(request, response); else if (URLString.endsWith("update-topic")) updateTopic(request, response); else if (URLString.endsWith("delete-topic")) deleteTopic(request, response); else if (URLString.endsWith("add-type-listener")) addTypeListener(request, response); else if (URLString.endsWith("remove-type-listener")) removeTypeListener(request, response); else if (URLString.endsWith("tolog-update")) tologUpdate(request, response); else reportError(response, "No such POST request" + URLString); } // --- TMRAP request implementations private void getTopicPage(HttpServletRequest request, HttpServletResponse response) throws IOException { try { // get context NavigatorApplicationIF navapp = NavigatorUtils.getNavigatorApplication(getServletContext()); // get parameters Collection<LocatorIF> indicators = getIndicators(request); Collection<LocatorIF> items = getItemIdentifiers(request); Collection<LocatorIF> subjects = getSubjectLocators(request); String allowedSyntaxes[] = new String[]{SYNTAX_XTM}; getParameter(request, response, "get-topic-page", SYNTAX_PARAMETER_NAME, false, allowedSyntaxes, SYNTAX_XTM); String[] tmids = request.getParameterValues(TOPICMAP_PARAMETER_NAME); TopicMapIF tm = TMRAPImplementation.getTopicPage(navapp, rapconfig, items, subjects, indicators, tmids); // write the response response.setContentType("application/xml; charset=utf-8"); new XTMTopicMapWriter(response.getWriter(), "utf-8").write(tm); } catch (Exception e) { reportError(response, e); } } /** * Get a tolog query result. */ private void getTolog(HttpServletRequest request, HttpServletResponse response) throws IOException { try { // get context NavigatorApplicationIF navapp = NavigatorUtils.getNavigatorApplication(getServletContext()); // set up parameters String query = request.getParameter(TOLOG_PARAMETER_NAME); String tmid = request.getParameter(TOPICMAP_PARAMETER_NAME); String syntax = request.getParameter(SYNTAX_PARAMETER_NAME); String view = request.getParameter(VIEW_PARAMETER_NAME); String compress_string = request.getParameter(COMPRESS_PARAMETER_NAME); boolean compress = compress_string != null && compress_string.equals("true"); // invoke real implementation if (compress) { response.setContentType("application/x-gzip"); GZIPOutputStream out = new GZIPOutputStream(response.getOutputStream()); PrettyPrinter pp = new PrettyPrinter(out); TMRAPImplementation.getTolog(navapp, query, tmid, syntax, view, pp); out.finish(); // ensures we get a complete gzip stream... } else { // not compressed response.setContentType("text/xml; charset=utf-8"); PrettyPrinter pp = new PrettyPrinter(response.getWriter(), "utf-8"); TMRAPImplementation.getTolog(navapp, query, tmid, syntax, view, pp); } } catch (Exception e) { reportError(response, e); } } /** * Add a fragment to a topic map. */ private void addFragment(HttpServletRequest request, HttpServletResponse response) throws IOException { try { // get context NavigatorApplicationIF navapp = NavigatorUtils.getNavigatorApplication(getServletContext()); // set up parameters String syntax = request.getParameter(SYNTAX_PARAMETER_NAME); String fragment = request.getParameter(FRAGMENT_PARAMETER_NAME); String tmid = request.getParameter(TOPICMAP_PARAMETER_NAME); TMRAPImplementation.addFragment(navapp, fragment, syntax, tmid); } catch (Exception e) { reportError(response, e); } } /** * Update a topic with a fragment. */ private void updateTopic(HttpServletRequest request, HttpServletResponse response) throws IOException { try { // get context NavigatorApplicationIF navapp = NavigatorUtils.getNavigatorApplication(getServletContext()); // set up parameters String syntax = request.getParameter(SYNTAX_PARAMETER_NAME); String fragment = request.getParameter(FRAGMENT_PARAMETER_NAME); String tmid = request.getParameter(TOPICMAP_PARAMETER_NAME); Collection<LocatorIF> indicators = getIndicators(request); Collection<LocatorIF> items = getItemIdentifiers(request); Collection<LocatorIF> subjects = getSubjectLocators(request); TMRAPImplementation.updateTopic(navapp, fragment, syntax, tmid, indicators, items, subjects); } catch (Exception e) { reportError(response, e); } } /** * Delete a topic. */ private void deleteTopic(HttpServletRequest request, HttpServletResponse response) throws IOException { try { // get context NavigatorApplicationIF navapp = NavigatorUtils.getNavigatorApplication(getServletContext()); // set up parameters Collection<LocatorIF> subjectIndicators = getIndicators(request); Collection<LocatorIF> sourceLocators = getItemIdentifiers(request); Collection<LocatorIF> subjectLocators = getSubjectLocators(request); String[] tmids = request.getParameterValues(TOPICMAP_PARAMETER_NAME); String msg = TMRAPImplementation.deleteTopic(navapp, sourceLocators, subjectLocators, subjectIndicators, tmids); response.setContentType("text/plain; charset=us-ascii"); response.getWriter().write(msg); } catch (Exception e) { reportError(response, e); } } /** * Write XTM response for topic fragment. The requested topic is * serialized as a fragment. If more than one topic is located then * a unifying topic is added at the end of the XTM fragment. This * topic has all the identities contained in the request. */ private void getTopic(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // get context NavigatorApplicationIF navapp = NavigatorUtils.getNavigatorApplication(getServletContext()); try { // fetch topic identity uris from request parameters Collection<LocatorIF> indicators = getIndicators(request); Collection<LocatorIF> items = getItemIdentifiers(request); Collection<LocatorIF> subjects = getSubjectLocators(request); String[] tmids = request.getParameterValues(TOPICMAP_PARAMETER_NAME); String syntax = request.getParameter(SYNTAX_PARAMETER_NAME); String view = request.getParameter(VIEW_PARAMETER_NAME); // call real implementation response.setContentType("text/xml; charset=utf-8"); PrettyPrinter pp = new PrettyPrinter(response.getWriter(), "utf-8"); TMRAPImplementation.getTopic(navapp, items, subjects, indicators, tmids, syntax, view, pp); } catch (Exception e) { reportError(response, e); } } private void addTypeListener(HttpServletRequest request, HttpServletResponse response) throws IOException { // ----------------------------------------------------------------------- // | Parameter | Required? | Repeatable? | Type | Value | Default | // ----------------------------------------------------------------------- // | item | no | yes | URI | | | // | subject | no | yes | URI | | | // | identifier | no | yes | URI | | | // | topicmap | yes | no | String | | | // | client | yes | no | Handle | | | // | syntax | no | no | String | | | // ----------------------------------------------------------------------- // (+) means other values may be allowed later. TopicIndexIF topicIndex = null; try { // fetch topic identity uris from request parameters Collection<LocatorIF> subjectIndicators = getIndicators(request); Collection<LocatorIF> sourceLocators = getItemIdentifiers(request); Collection<LocatorIF> subjectLocators = getSubjectLocators(request); // Check that the topicmap parameter was given (since it's required). getParameter(request, response, "add-type-listener", TOPICMAP_PARAMETER_NAME, true, null, null); // Once supported, syntax will determine the output syntax. String allowedSyntaxes[] = new String[]{SYNTAX_XTM}; String syntax = getParameter(request, response, "add-type-listener", SYNTAX_PARAMETER_NAME, false, allowedSyntaxes, SYNTAX_XTM); String client = getParameter(request, response, "add-type-listener", CLIENT_PARAMETER_NAME, false, null, SYNTAX_XTM); // get topic(s) topicIndex = getTopicIndex(request.getParameterValues( TOPICMAP_PARAMETER_NAME)); Collection<TopicIF> topics = topicIndex.getTopics(subjectIndicators, sourceLocators, subjectLocators); if (topics.size() != 1) reportError(response, "add-type-listener: Wrong number of topics."); TopicIF topic = topics.iterator().next(); Map<String, String> currentTypeListeners = clientListeners.get(topic); if (currentTypeListeners == null) { currentTypeListeners = new HashMap<String, String>(); clientListeners.put(topic, currentTypeListeners); } // Register the client as a listener for topics of type 'topic'. currentTypeListeners.put(client, syntax); } catch (RAPServletException e) { reportError(response, e); } finally { closeIndex(topicIndex); } } private void removeTypeListener(HttpServletRequest request, HttpServletResponse response) throws IOException { // ----------------------------------------------------------------------- // | Parameter | Required? | Repeatable? | Type | Value | Default | // ----------------------------------------------------------------------- // | item | no | yes | URI | | | // | subject | no | yes | URI | | | // | identifier | no | yes | URI | | | // | topicmap | yes | no | String | | | // | client | yes | no | Handle | | | // | syntax | no | no | String | | | // ----------------------------------------------------------------------- // (+) means other values may be allowed later. TopicIndexIF topicIndex = null; try { // fetch topic identity uris from request parameters Collection<LocatorIF> subjectIndicators = getIndicators(request); Collection<LocatorIF> sourceLocators = getItemIdentifiers(request); Collection<LocatorIF> subjectLocators = getSubjectLocators(request); // Check that the topicmap parameter was given (since it's required). getParameter(request, response, "remove-type-listener", TOPICMAP_PARAMETER_NAME, true, null, null); String client = getParameter(request, response, "remove-type-listener", CLIENT_PARAMETER_NAME, false, null, SYNTAX_XTM); // get topic(s) topicIndex = getTopicIndex(request.getParameterValues( TOPICMAP_PARAMETER_NAME)); Collection<TopicIF> topics = topicIndex.getTopics(subjectIndicators, sourceLocators, subjectLocators); if (topics.size() != 1) reportError(response, "remove-type-listener: Wrong number of topics."); TopicIF topic = topics.iterator().next(); Map<String, String> currentTypeListeners = clientListeners.get(topic); if (currentTypeListeners == null) reportError(response, "remove-type-listener: " + "Listener not found. You have to register a listener before it can " + "be removed."); String currentListener = currentTypeListeners.remove(client); if (currentListener == null) reportError(response, "remove-type-listener: " + "Listener not found. You have to register a listener before it can " + "be removed."); } catch (RAPServletException e) { reportError(response, e); } finally { closeIndex(topicIndex); } } /** * Run a tolog update statement. */ private void tologUpdate(HttpServletRequest request, HttpServletResponse response) throws IOException { try { // get context NavigatorApplicationIF navapp = NavigatorUtils.getNavigatorApplication(getServletContext()); // set up parameters String[] tmids = request.getParameterValues(TOPICMAP_PARAMETER_NAME); if (tmids.length != 1) { reportError(response, "tolog-update: Exactly one topic map ID required"); return; } String stmt = request.getParameter(STATEMENT_PARAMETER_NAME); int rows = TMRAPImplementation.tologUpdate(navapp, tmids[0], stmt); response.setContentType("text/plain; charset=us-ascii"); response.getWriter().write("" + rows); } catch (Exception e) { reportError(response, e); } } // --- Internal helpers /** * Gets and validates a request parameter. * @param request The source of the parameters. * @param response The receiver of any error messages. * @param operationName The name or the calling operation. * @param parameterName The name of the parameter. * @param required true iff this parameter is required. * @param supported true iff this parameter is supported (allows others than the default value). * @param defaultValue The value used if no parameter value is found. * @return The parameter value, or defaultValue if it cannot be found. * @throws IOException If an error occurs and the error reporting doesn't work */ private String getParameter(HttpServletRequest request, HttpServletResponse response, String operationName, String parameterName, boolean required, String supported[], String defaultValue) throws RAPServletException { String parameters[] = request.getParameterValues(parameterName); if (parameters == null || parameters.length == 0) { if (required) throw new RAPServletException("The '" + parameterName + "'-parameter is required for the " + operationName + " operation."); return defaultValue; } else if (parameters.length == 1) { String parameter = parameters[0]; if (!(supported == null || Arrays.asList(supported).contains(parameter))) { throw new RAPServletException("The '" + parameterName + "'-parameter of the " + operationName + " does not support the value: \"" + parameter + "\". The supported values are " + makeSeparatedWords(supported, ", ", " and ")); } return parameter; } // Never suport repeated values. throw new RAPServletException("The '" + parameterName + "'-parameter of the " + operationName + " operation does not" + " support repeated values."); } private TopicIndexIF getTopicIndex(String[] tmids) throws RAPServletException { NavigatorApplicationIF navApp = NavigatorUtils.getNavigatorApplication(getServletContext()); if (tmids == null || tmids.length == 0) return new RegistryTopicIndex(navApp.getTopicMapRepository(), true, rapconfig.getEditURI(), rapconfig.getViewURI()); List<TopicIndexIF> topicIndexes = new ArrayList<TopicIndexIF>(); for (int i = 0; i < tmids.length; i++) { TopicMapIF topicmap; try { topicmap = navApp.getTopicMapById(tmids[i], true); } catch (NavigatorRuntimeException e) { log.warn("Couldn't open topic map " + tmids[i] + " because of " + e.getClass().getName() + " with message: " + e.getMessage()); throw new RAPServletException("Couldn't open topic map " + tmids[i]); } TopicIndexIF currentIndex = new TopicMapTopicIndex(topicmap, rapconfig.getEditURI(), rapconfig.getViewURI(), tmids[i]); topicIndexes.add(currentIndex); } return new FederatedTopicIndex(topicIndexes); } private Collection<LocatorIF> getURICollection(HttpServletRequest request, String paramName) throws RAPServletException { String[] value = request.getParameterValues(paramName); if (value == null) return Collections.emptySet(); HashSet<LocatorIF> uriLocators = new HashSet<LocatorIF>(); for (int i = 0; i < value.length; i++) { try { uriLocators.add(new URILocator(value[i])); } catch (MalformedURLException e) { log.warn("MalformedURL: " + value[i]); throw new RAPServletException("Malformed URL: " + value[i]); } } return uriLocators; } private Collection<LocatorIF> getIndicators(HttpServletRequest request) throws RAPServletException { return getURICollection(request, INDICATOR_PARAMETER_NAME); } private Collection<LocatorIF> getItemIdentifiers(HttpServletRequest request) throws RAPServletException { return getURICollection(request, SOURCE_PARAMETER_NAME); } private Collection<LocatorIF> getSubjectLocators(HttpServletRequest request) throws RAPServletException { return getURICollection(request, SUBJECT_PARAMETER_NAME); } private class RAPServletException extends Exception { // Much wanted by Serializable. (The number is randomly typed). private static final long serialVersionUID = 7912425438445764224l; String message; public RAPServletException(String message) { this.message = message; log.warn(message, this); } public String getMessage() { return message; } } private String makeSeparatedWords(String[] words, String separator, String lastSeparator) { if (words.length == 0) return ""; if (words.length == 1) return words[0]; int length = words.length; String retVal = words[length - 2] + lastSeparator + words[length - 1]; for (int i = length - 3; i >= 0; i--) { retVal = words[i] + separator + retVal; } return retVal; } private void closeIndex(TopicIndexIF topicIndex) { if (topicIndex != null) topicIndex.close(); } private void reportError(HttpServletResponse response, String message) throws IOException { log.warn(message); try { response.sendError(400, message); } catch (IOException e) { log.warn("Failed to report error: " + message + " because sendError gave " + IOException.class.getName()); throw e; } } private void reportError(HttpServletResponse response, Throwable t) throws IOException { log.warn("Error occurred.", t); try { response.sendError(400, t.toString()); } catch (IOException e) { log.warn("Failed to report error: " + t.getMessage() + " because sendError gave " + IOException.class.getName()); throw e; } } }