/* LanguageTool, a natural language style checker * Copyright (C) 2012 Daniel Naber (http://www.danielnaber.de) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 * USA */ package org.languagetool.server; import org.apache.commons.lang3.ArrayUtils; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Properties; /** * @since 2.0 */ public class HTTPServerConfig { enum Mode { LanguageTool } public static final String DEFAULT_HOST = "localhost"; /** The default port on which the server is running (8081). */ public static final int DEFAULT_PORT = 8081; static final String LANGUAGE_MODEL_OPTION = "--languageModel"; protected boolean verbose = false; protected boolean publicAccess = false; protected int port = DEFAULT_PORT; protected String allowOriginUrl = null; protected int maxTextLength = Integer.MAX_VALUE; protected long maxCheckTimeMillis = -1; protected int maxCheckThreads = 10; protected Mode mode; protected File languageModelDir = null; protected int requestLimit; protected int requestLimitPeriodInSeconds; protected boolean trustXForwardForHeader; protected int maxWorkQueueSize; protected File rulesConfigFile = null; protected int cacheSize = 0; protected boolean warmUp = false; /** * Create a server configuration for the default port ({@link #DEFAULT_PORT}). */ public HTTPServerConfig() { this(DEFAULT_PORT, false); } /** * @param serverPort the port to bind to * @since 2.8 */ public HTTPServerConfig(int serverPort) { this(serverPort, false); } /** * @param serverPort the port to bind to * @param verbose when set to <tt>true</tt>, the input text will be logged in case there is an exception */ public HTTPServerConfig(int serverPort, boolean verbose) { this.port = serverPort; this.verbose = verbose; } /** * Parse command line options. */ HTTPServerConfig(String[] args) { for (int i = 0; i < args.length; i++) { if (args[i].matches("--[a-zA-Z]+=.+")) { System.err.println("WARNING: use `--option value`, not `--option=value`, parameters will be ignored otherwise: " + args[i]); } switch (args[i]) { case "--config": parseConfigFile(new File(args[++i]), !ArrayUtils.contains(args, LANGUAGE_MODEL_OPTION)); break; case "-p": case "--port": port = Integer.parseInt(args[++i]); break; case "-v": case "--verbose": verbose = true; break; case "--public": publicAccess = true; break; case "--allow-origin": allowOriginUrl = args[++i]; if (allowOriginUrl.startsWith("--")) { throw new IllegalArgumentException("Missing argument for '--allow-origin'"); } break; case LANGUAGE_MODEL_OPTION: setLanguageModelDirectory(args[++i]); break; } } } private void parseConfigFile(File file, boolean loadLangModel) { try { Properties props = new Properties(); try (FileInputStream fis = new FileInputStream(file)) { props.load(fis); maxTextLength = Integer.parseInt(getOptionalProperty(props, "maxTextLength", Integer.toString(Integer.MAX_VALUE))); maxCheckTimeMillis = Long.parseLong(getOptionalProperty(props, "maxCheckTimeMillis", "-1")); requestLimit = Integer.parseInt(getOptionalProperty(props, "requestLimit", "0")); requestLimitPeriodInSeconds = Integer.parseInt(getOptionalProperty(props, "requestLimitPeriodInSeconds", "0")); trustXForwardForHeader = Boolean.valueOf(getOptionalProperty(props, "trustXForwardForHeader", "false")); maxWorkQueueSize = Integer.parseInt(getOptionalProperty(props, "maxWorkQueueSize", "0")); if (maxWorkQueueSize < 0) { throw new IllegalArgumentException("maxWorkQueueSize must be >= 0: " + maxWorkQueueSize); } String langModel = getOptionalProperty(props, "languageModel", null); if (langModel != null && loadLangModel) { setLanguageModelDirectory(langModel); } maxCheckThreads = Integer.parseInt(getOptionalProperty(props, "maxCheckThreads", "10")); if (maxCheckThreads < 1) { throw new IllegalArgumentException("Invalid value for maxCheckThreads, must be >= 1: " + maxCheckThreads); } boolean atdMode = getOptionalProperty(props, "mode", "LanguageTool").equalsIgnoreCase("AfterTheDeadline"); if (atdMode) { throw new IllegalArgumentException("The AfterTheDeadline mode is not supported anymore in LanguageTool 3.8 or later"); } String rulesConfigFilePath = getOptionalProperty(props, "rulesFile", null); if (rulesConfigFilePath != null) { rulesConfigFile = new File(rulesConfigFilePath); if (!rulesConfigFile.exists() || !rulesConfigFile.isFile()) { throw new RuntimeException("Rules Configuration file can not be found: " + rulesConfigFile); } } cacheSize = Integer.parseInt(getOptionalProperty(props, "cacheSize", "0")); if (cacheSize < 0) { throw new IllegalArgumentException("Invalid value for cacheSize: " + cacheSize + ", use 0 to deactivate cache"); } String warmUpStr = getOptionalProperty(props, "warmUp", "false"); if (warmUpStr.equals("true")) { warmUp = true; } else if (warmUpStr.equals("false")) { warmUp = false; } else { throw new IllegalArgumentException("Invalid value for warmUp: '" + warmUpStr + "', use 'true' or 'false'"); } } } catch (IOException e) { throw new RuntimeException("Could not load properties from '" + file + "'", e); } } private void setLanguageModelDirectory(String langModelDir) { languageModelDir = new File(langModelDir); if (!languageModelDir.exists() || !languageModelDir.isDirectory()) { throw new RuntimeException("LanguageModel directory not found or is not a directory: " + languageModelDir); } } /* * @param verbose if true, the text to be checked will be displayed in case of exceptions */ public boolean isVerbose() { return verbose; } public boolean isPublicAccess() { return publicAccess; } public int getPort() { return port; } /** * Value to set as the "Access-Control-Allow-Origin" http header. {@code null} * will not return that header at all. With {@code *} your server can be used from any other web site * from Javascript/Ajax (search Cross-origin resource sharing (CORS) for details). */ @Nullable public String getAllowOriginUrl() { return allowOriginUrl; } /** * @param maxTextLength the maximum text length allowed (in number of characters), texts that are longer * will cause an exception when being checked */ public void setMaxTextLength(int maxTextLength) { this.maxTextLength = maxTextLength; } int getMaxTextLength() { return maxTextLength; } int getRequestLimit() { return requestLimit; } int getRequestLimitPeriodInSeconds() { return requestLimitPeriodInSeconds; } /** * @param maxCheckTimeMillis The maximum duration allowed for a single check in milliseconds, checks that take longer * will stop with an exception. Use {@code -1} for no limit. * @since 2.6 */ void setMaxCheckTimeMillis(int maxCheckTimeMillis) { this.maxCheckTimeMillis = maxCheckTimeMillis; } /** @since 2.6 */ long getMaxCheckTimeMillis() { return maxCheckTimeMillis; } /** * Get language model directory (which contains '3grams' sub directory) or {@code null}. * @since 2.7 */ @Nullable File getLanguageModelDir() { return languageModelDir; } /** @since 2.7 */ Mode getMode() { return mode; } /** * @param maxCheckThreads The maximum number of threads serving requests running at the same time. * If there are more requests, they will be queued until a thread can work on them. * @since 2.7 */ void setMaxCheckThreads(int maxCheckThreads) { this.maxCheckThreads = maxCheckThreads; } /** @since 2.7 */ int getMaxCheckThreads() { return maxCheckThreads; } /** * Set to {@code true} if this is running behind a (reverse) proxy which * sets the {@code X-forwarded-for} HTTP header. The last IP address (but not local IP addresses) * in that header will then be used for enforcing a request limitation. * @since 2.8 */ void setTrustXForwardForHeader(boolean trustXForwardForHeader) { this.trustXForwardForHeader = trustXForwardForHeader; } /** @since 2.8 */ boolean getTrustXForwardForHeader() { return trustXForwardForHeader; } /** @since 2.9 */ int getMaxWorkQueueSize() { return maxWorkQueueSize; } /** @since 3.7 */ int getCacheSize() { return cacheSize; } /** @since 3.7 */ boolean getWarmUp() { return warmUp; } /** * @return the file from which server rules configuration should be loaded, or {@code null} * @since 3.0 */ @Nullable File getRulesConfigFile() { return rulesConfigFile; } /** * @throws IllegalConfigurationException if property is not set */ protected String getProperty(Properties props, String propertyName, File config) { String propertyValue = (String)props.get(propertyName); if (propertyValue == null || propertyValue.trim().isEmpty()) { throw new IllegalConfigurationException("Property '" + propertyName + "' must be set in " + config); } return propertyValue; } protected String getOptionalProperty(Properties props, String propertyName, String defaultValue) { String propertyValue = (String)props.get(propertyName); if (propertyValue == null) { return defaultValue; } return propertyValue; } }