package org.ovirt.engine.docs.utils.servlet; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.ObjectNode; import org.ovirt.engine.core.utils.EngineLocalConfig; import org.ovirt.engine.core.utils.servlet.ServletUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This servlet serves the context-sensitve help mapping files (JSON format) to the web UI. * * Each application has documentation in multiple locales, and each locale may have multiple mapping files. * * The servlet handles loading and compressing all of that information into one JSON file per application. * * Roughly: * * Get the directory where the context-sensitive help package is installed * * Detect which locales are installed * * For each locale * * get the conf.d directory * * for each application subdir (webadmin, userportal, etc) * * Look for json mapping files * * concatenate them into a single json file for this locale+application * * concatenate all of the locale+application jsons into one giant json per application * * This happens for every request of an application's json mapping file, which is once per user login. * * @see ContextSensitiveHelpManager for the client-side portion of this operation. */ public class ContextSensitiveHelpMappingServlet extends HttpServlet { private static final long serialVersionUID = -393894763659009626L; private static final Logger log = LoggerFactory.getLogger(ContextSensitiveHelpMappingServlet.class); private static final String MANUAL_DIR_KEY = "manualDir"; //$NON-NLS-1$ private static final String CSH_MAPPING_DIR = "csh.conf.d"; //$NON-NLS-1$ private static final String JSON = ".json"; //$NON-NLS-1$ // parse xxxxxx from /some/path/xxxxxx.json private static Pattern REQUEST_PATTERN = Pattern.compile(".*?(?<key>[^/]*)\\.json"); //$NON-NLS-1$ private static Pattern LOCALE_PATTERN = Pattern.compile("\\w\\w-\\w\\w"); //$NON-NLS-1$ private static ObjectMapper mapper = new ObjectMapper(); /** * Respond to a GET request for the CSH mapping file. See class Javadoc for the algorithm. * * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String application = getApplication(request); if (application == null) { log.warn("ContextSensitiveHelpMappingServlet could not handle request. URL = " + request.getRequestURI()); //$NON-NLS-1$ return; } String manualPath = getManualDir(getServletConfig()); List<String> locales = getLocales(new File(manualPath)); ObjectNode appCsh = mapper.createObjectNode(); for (String locale : locales) { File cshConfigDir = new File(manualPath + "/" + locale + "/" + CSH_MAPPING_DIR + "/" + application); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ if (cshConfigDir.exists() && cshConfigDir.canRead()) { List<JsonNode> configData = readJsonFiles(cshConfigDir); // merge the data from all the files if (configData.size() > 0) { JsonNode destination = configData.get(0); for (int i = 1; i < configData.size(); i++) { destination = merge(destination, configData.get(i)); } appCsh.put(locale, destination); } } else { log.error("couldn't get csh directory: " + cshConfigDir); //$NON-NLS-1$ } } response.setContentType("application/json"); //$NON-NLS-1$ PrintStream printStream = new PrintStream(response.getOutputStream()); printStream.print(appCsh.toString()); printStream.flush(); } /** * parse xxxxxx from /some/path/xxxxxx.json */ protected String getApplication(HttpServletRequest request) { Matcher m = REQUEST_PATTERN.matcher(request.getRequestURI()); if (m.matches()) { return m.group("key"); //$NON-NLS-1$ } return null; } /** * Read the json files from the directory. * * @param configPath directory to read * @return List<JsonNode> containing each file read */ protected List<JsonNode> readJsonFiles(File configPath) { List<JsonNode> nodes = new ArrayList<>(); List<String> jsonFiles = getJsonFiles(configPath); for (String jsonFile : jsonFiles) { File file = new File(configPath, jsonFile); if (file.exists() && file.canRead()) { try (BufferedReader reader = new BufferedReader(new FileReader(file.getAbsolutePath()))) { nodes.add(mapper.readTree(reader)); } catch (IOException e) { log.error("Exception parsing documentation mapping file '{}': {}", //$NON-NLS-1$ file.getAbsolutePath(), e.getMessage()); log.error("Exception: ", e); //$NON-NLS-1$ } } } return nodes; } /** * Return sorted list of names of the json config files in the config dir. * * @param configDir directory to search * @return sorted list of file names */ protected List<String> getJsonFiles(File configDir) { List<String> jsonFiles = new ArrayList<>(); if (!configDir.exists() || !configDir.canRead()) { log.error("csh configDir doesn't exist: " + configDir); //$NON-NLS-1$ return jsonFiles; } File[] configFiles = configDir.listFiles(); if (configFiles != null) { for (File configFile : configFiles) { if (configFile.isFile() && configFile.canRead() && configFile.getName().endsWith(JSON)) { jsonFiles.add(configFile.getName()); } } } // last file wins Collections.sort(jsonFiles); return jsonFiles; } /** * Get List of the installed documentation locales. * * @param manualDir directory to search */ protected List<String> getLocales(File manualDir) { List<String> locales = new ArrayList<>(); if (!manualDir.exists() || !manualDir.canRead()) { return locales; } File[] manualFiles = manualDir.listFiles(); if (manualFiles != null) { for (File dir : manualFiles) { if (dir.isDirectory() && dir.canRead()) { String name = dir.getName(); Matcher m = LOCALE_PATTERN.matcher(name); if (m.matches()) { locales.add(name); } } } } return locales; } /** * Get the configured manual directory from the servlet config. */ protected String getManualDir(ServletConfig config) { EngineLocalConfig engineLocalConfig = EngineLocalConfig.getInstance(); String manualDir = ServletUtils.getAsAbsoluteContext(getServletContext().getContextPath(), engineLocalConfig.expandString( config.getInitParameter(MANUAL_DIR_KEY).replaceAll("%\\{", "\\${")) //$NON-NLS-1$ //$NON-NLS-2$ ); return manualDir; } /** * Merge json objects. This is used to put json mappings from multiple config files into one object. * * Note that this method is recursive. * * @param destination destination json node * @param source source json node. * @return merged json object */ protected static JsonNode merge(JsonNode destination, JsonNode source) { Iterator<String> fieldNames = source.getFieldNames(); while (fieldNames.hasNext()) { String fieldName = fieldNames.next(); JsonNode jsonNode = destination.get(fieldName); // if field is an embedded object, recurse if (jsonNode != null && jsonNode.isObject()) { merge(jsonNode, source.get(fieldName)); } // else it's a plain field else if (destination instanceof ObjectNode) { // overwrite field JsonNode value = source.get(fieldName); ((ObjectNode) destination).put(fieldName, value); } } return destination; } }