/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.build;

import java.util.List;
import java.util.LinkedList;

/**
 * Utility class which can tokenize a String into a list of String arguments,
 * with behavior similar to parsing command line arguments to a program.
 * Quoted Strings are treated as single arguments, and escaped characters
 * are translated so that the tokenized arguments have the same meaning.
 * Since all methods are static, the class is declared abstract to prevent
 * instantiation.
 * @version $Id$
 */
public abstract class ArgumentTokenizer {
  private static final int NO_TOKEN_STATE = 0;
  private static final int NORMAL_TOKEN_STATE = 1;
  private static final int SINGLE_QUOTE_STATE = 2;
  private static final int DOUBLE_QUOTE_STATE = 3;

  /** Tokenizes the given String into String tokens
    * @param arguments A String containing one or more command-line style arguments to be tokenized.
    * @return A list of parsed and properly escaped arguments.
    */
  public static List<String> tokenize(String arguments) {
    return tokenize(arguments, false);
  }

  /** Tokenizes the given String into String tokens.
    * @param arguments A String containing one or more command-line style arguments to be tokenized.
    * @param stringify whether or not to include escape special characters
    * @return A list of parsed and properly escaped arguments.
    */
  public static List<String> tokenize(String arguments, boolean stringify) {

    LinkedList<String> argList = new LinkedList<>();
    StringBuilder currArg = new StringBuilder();
    boolean escaped = false;
    int state = NO_TOKEN_STATE;  // start in the NO_TOKEN_STATE
    int len = arguments.length();

    // Loop over each character in the string
    for (int i = 0; i < len; i++) {
      char c = arguments.charAt(i);
      if (escaped) {
        // Escaped state: just append the next character to the current arg.
        escaped = false;
        currArg.append(c);
      }
      else {
        switch(state) {
          case SINGLE_QUOTE_STATE:
            if (c == '\'') {
            // Seen the close quote; continue this arg until whitespace is seen
            state = NORMAL_TOKEN_STATE;
          }
            else {
              currArg.append(c);
            }
            break;
          case DOUBLE_QUOTE_STATE:
            if (c == '"') {
            // Seen the close quote; continue this arg until whitespace is seen
            state = NORMAL_TOKEN_STATE;
          }
            else if (c == '\\') {
              // Look ahead, and only escape quotes or backslashes
              i++;
              char next = arguments.charAt(i);
              if (next == '"' || next == '\\') {
                currArg.append(next);
              }
              else {
                currArg.append(c);
                currArg.append(next);
              }
            }
            else {
              currArg.append(c);
            }
            break;
//          case NORMAL_TOKEN_STATE:
//            if (Character.isWhitespace(c)) {
//              // Whitespace ends the token; start a new one
//              argList.add(currArg.toString());
//              currArg = new StringBuffer();
//              state = NO_TOKEN_STATE;
//            }
//            else if (c == '\\') {
//              // Backslash in a normal token: escape the next character
//              escaped = true;
//            }
//            else if (c == '\'') {
//              state = SINGLE_QUOTE_STATE;
//            }
//            else if (c == '"') {
//              state = DOUBLE_QUOTE_STATE;
//            }
//            else {
//              currArg.append(c);
//            }
//            break;
          case NO_TOKEN_STATE:
          case NORMAL_TOKEN_STATE:
            switch(c) {
            case '\\':
              escaped = true;
              state = NORMAL_TOKEN_STATE;
              break;
            case '\'':
              state = SINGLE_QUOTE_STATE;
              break;
            case '"':
              state = DOUBLE_QUOTE_STATE;
              break;
            default:
              if (!Character.isWhitespace(c)) {
              currArg.append(c);
              state = NORMAL_TOKEN_STATE;
            }
              else if (state == NORMAL_TOKEN_STATE) {
                // Whitespace ends the token; start a new one
                argList.add(currArg.toString());
                currArg = new StringBuilder();
                state = NO_TOKEN_STATE;
              }
          }
            break;
          default:
            throw new IllegalStateException("ArgumentTokenizer state " + state + " is invalid!");
        }
      }
    }

    // If we're still escaped, put in the backslash
    if (escaped) {
      currArg.append('\\');
      argList.add(currArg.toString());
    }
    // Close the last argument if we haven't yet
    else if (state != NO_TOKEN_STATE) {
      argList.add(currArg.toString());
    }
    // Format each argument if we've been told to stringify them
    if (stringify) {
      for (int i = 0; i < argList.size(); i++) {
        argList.set(i, "\"" + _escapeQuotesAndBackslashes(argList.get(i)) + "\"");
      }
    }
    return argList;
  }

  /** Inserts backslashes before any occurrences of a backslash or
   * quote in the given string.  Also converts any special characters
   * appropriately.
   */
  protected static String _escapeQuotesAndBackslashes(String s) {
    final StringBuilder buf = new StringBuilder(s);

    // Walk backwards, looking for quotes or backslashes.
    //  If we see any, insert an extra backslash into the buffer at
    //  the same index.  (By walking backwards, the index into the buffer
    //  will remain correct as we change the buffer.)
    for (int i = s.length()-1; i >= 0; i--) {
      char c = s.charAt(i);
      if ((c == '\\') || (c == '"')) {
        buf.insert(i, '\\');
      }
      // Replace any special characters with escaped versions
      else if (c == '\n') {
        buf.deleteCharAt(i);
        buf.insert(i, "\\n");
      }
      else if (c == '\t') {
        buf.deleteCharAt(i);
        buf.insert(i, "\\t");
      }
      else if (c == '\r') {
        buf.deleteCharAt(i);
        buf.insert(i, "\\r");
      }
      else if (c == '\b') {
        buf.deleteCharAt(i);
        buf.insert(i, "\\b");
      }
      else if (c == '\f') {
        buf.deleteCharAt(i);
        buf.insert(i, "\\f");
      }
    }
    return buf.toString();
  }
}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.utils;

public class HttpHeaderUtils {

    /**
     * A http content type should contain a character set like
     * "application/json; charset=utf-8".
     *
     * If you only want to get "application/json" you can use this method.
     *
     * See also: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
     *
     * @param rawContentType "application/json; charset=utf-8" or "application/json"
     * @return only the contentType without charset. Eg "application/json"
     */
    public static String getContentTypeFromContentTypeAndCharacterSetting(String rawContentType) {

        if (rawContentType.contains(";")) {
            return rawContentType.split(";")[0];
        } else {
            return rawContentType;
        }

    }

    /**
     * A http content type should contain a character set like
     * "application/json; charset=utf-8".
     *
     * If you only want to get the character set you can use this method.
     *
     * See also: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
     *
     * @param rawContentType "application/json; charset=utf-8" or "application/json"
     * @return only the character set like utf-8 OR the defaultEncoding when not set
     */
    public static String getCharsetOfContentType(String rawContentType, String defaultEncoding) {

        if (rawContentType.contains("charset=")) {
            String charset = rawContentType.split("charset=") [1];

            return charset;
        } else {
            return defaultEncoding;
        }

    }

    /**
     * A http content type should contain a character set like
     * "application/json; charset=utf-8".
     *
     * If you only want to get the character set you can use this method.
     *
     * See also: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
     *
     * @param rawContentType "application/json; charset=utf-8" or "application/json"
     * @return only the character set like utf-8 OR utf-8 when not set
     */
    public static String getCharsetOfContentTypeOrUtf8(String rawContentType) {
        return getCharsetOfContentType(rawContentType, NinjaConstant.UTF_8);

    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;

/**
 * Parameter in a Route.
 *
 * @author Joe Lauer
 */
public class RouteParameter {

    // eg. {id: [0-9]+}
    private final int index;    // index of where token starts
    private final String token; // "{id: [0-9]+}"
    private final String name;  // "id"
    private final String regex; // "[0-9]+"

    public RouteParameter(int index, String token, String name, String regex) {
        this.index = index;
        this.token = token;
        this.name = name;
        this.regex = regex;
    }

    /**
     * Gets the index of where the token starts in the original uri.
     * @return An index of where the token is
     */
    public int getIndex() {
        return index;
    }

    /**
     * The exact string of the parameter such as "{id: [0-9]+}" in "{id: [0-9]+}"
     * @return The parameter token
     */
    public String getToken() {
        return token;
    }

    /**
     * The name of the parameter such as "id" in "{id: [0-9]+}"
     * @return The name of the parameter
     */
    public String getName() {
        return name;
    }

    /**
     * The regex of the parameter such as "[0-9]+" in "{id: [0-9]+}"
     * @return The regex of the parameter or null if no regex was included
     *      for the parameter.
     */
    public String getRegex() {
        return regex;
    }

    /**
     * Parse a path such as "/user/{id: [0-9]+}/email/{addr}" for the named
     * parameters.
     * @param path The path to parse
     * @return A map containing the named parameters in the order they were
     *      parsed or null if no parameters were parsed.
     */
    static public Map<String, RouteParameter> parse(String path) {
        Map<String,RouteParameter> params = new LinkedHashMap<>();

        // extract any named parameters
        Matcher matcher = Route.PATTERN_FOR_VARIABLE_PARTS_OF_ROUTE.matcher(path);
        while (matcher.find()) {
            RouteParameter param = new RouteParameter(
                matcher.start(0), matcher.group(0), matcher.group(1), matcher.group(3));
            params.put(param.getName(), param);
        }

        return params;
    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.servlet;

import ninja.utils.AbstractContext;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import ninja.Cookie;
import ninja.Result;
import ninja.bodyparser.BodyParserEngineManager;
import ninja.params.ParamParsers;
import ninja.servlet.async.AsyncStrategy;
import ninja.servlet.async.AsyncStrategyFactoryHolder;
import ninja.session.FlashScope;
import ninja.session.Session;
import ninja.uploads.FileItem;
import ninja.uploads.FileItemProvider;
import ninja.uploads.FileProvider;
import ninja.uploads.NoFileItemProvider;
import ninja.utils.HttpHeaderUtils;
import ninja.utils.NinjaConstant;
import ninja.utils.NinjaProperties;
import ninja.utils.ResponseStreams;
import ninja.validation.Validation;

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.inject.Inject;
import com.google.inject.Injector;
import ninja.utils.ResultHandler;

/**
 * Ninja context for servlet environments.
 *
 * When modifying functionality for this class please carefully consider adding
 * it to AbstractContext first.  For example, instead of relying on
 * httpServletRequest.getHeader() you could reuse the existing
 * this.getHeader() and be able to implement your feature entirely
 * in AbstractContext.
 */
public class NinjaServletContext extends AbstractContext {
    static final private Logger logger = LoggerFactory.getLogger(NinjaServletContext.class);

    private final ResultHandler resultHandler;
    private ServletContext servletContext;
    private HttpServletRequest httpServletRequest;
    private HttpServletResponse httpServletResponse;

    private AsyncStrategy asyncStrategy;
    private final Object asyncLock = new Object();

    private boolean formFieldsProcessed = false;
    private Map<String, List<String>> formFieldsMap;
    private Map<String, List<FileItem>> fileFieldsMap;

    @Inject
    public NinjaServletContext(
            BodyParserEngineManager bodyParserEngineManager,
            FlashScope flashScope,
            NinjaProperties ninjaProperties,
            ResultHandler resultHandler,
            Session session,
            Validation validation,
            Injector injector,
            ParamParsers paramParsers) {

        super(bodyParserEngineManager,
              flashScope,
              ninjaProperties,
              session,
              validation,
              injector,
              paramParsers);

        this.resultHandler = resultHandler;
    }

    public void init(ServletContext servletContext,
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse) {

        this.servletContext = servletContext;
        this.httpServletRequest = httpServletRequest;
        this.httpServletResponse = httpServletResponse;

        enforceCorrectEncodingOfRequest();

        String contextPath = httpServletRequest.getContextPath();
        String requestPath = performGetRequestPath();

        super.init(contextPath, requestPath);
    }

    @Override
    public String getMethod() {
        return httpServletRequest.getMethod();
    }

    @Deprecated
    @Override
    public String getRequestUri() {
        return httpServletRequest.getRequestURI();
    }

    @Override
    public String getHostname() {
        return httpServletRequest.getHeader("host");
    }

    @Override
    public String getParameter(String key) {
        if (!formFieldsProcessed) processFormFields();
        if (formFieldsMap == null) {
            return httpServletRequest.getParameter(key);
        } else {
            List<String> values = formFieldsMap.get(key);
            if (values == null || values.isEmpty())
                return null;
            return values.get(0);
        }
    }

    @Override
    public List<String> getParameterValues(String name) {
        if (!formFieldsProcessed) processFormFields();
        if (formFieldsMap == null) {
            String[] params = httpServletRequest.getParameterValues(name);
            if (params == null) {
                return Collections.emptyList();
            }
            return Arrays.asList(params);
        } else {
            return formFieldsMap.get(name);
        }
    }

    @Override
    public String getScheme() {
        return httpServletRequest.getScheme();
    }

    @Override
    public String getRealRemoteAddr() {
        return httpServletRequest.getRemoteAddr();
    }

    @Override
    public Object getAttribute(String name) {
        return httpServletRequest.getAttribute(name);
    }

    @Override
    public void setAttribute(String name, Object value) {
        httpServletRequest.setAttribute(name, value);
    }

    @Override
    public Map<String, Object> getAttributes() {
        // build map of attributes
        Map<String,Object> attributes = new HashMap<>();

        Enumeration<String> en = httpServletRequest.getAttributeNames();

        while (en.hasMoreElements()) {
            String name = en.nextElement();
            Object value = httpServletRequest.getAttribute(name);
            attributes.put(name, value);
        }

        return attributes;
    }

    @Override
    public FileItem getParameterAsFileItem(String key) {
        if (!formFieldsProcessed) processFormFields();
        if (fileFieldsMap == null) return null;
        List<FileItem> fileItems = fileFieldsMap.get(key);
        if (fileItems == null || fileItems.isEmpty()) return null;
        return fileItems.get(0);
    }

    @Override
    public List<FileItem> getParameterAsFileItems(String key) {
        if (!formFieldsProcessed) processFormFields();
        if (fileFieldsMap == null) return Collections.emptyList();
        List<FileItem> fileItems = fileFieldsMap.get(key);
        if (fileItems == null) return Collections.emptyList();
        return fileItems;
    }

    @Override
    public Map<String, List<FileItem>> getParameterFileItems() {
        if (!formFieldsProcessed) processFormFields();
        return fileFieldsMap;
    }

    @Override
    public Map<String, String[]> getParameters() {
        if (!formFieldsProcessed) processFormFields();
        if (formFieldsMap == null) {
            return httpServletRequest.getParameterMap();
        } else {
            // convert List<String> value to String[] value
            String[] type = new String[0];
            Map<String, String[]> map = new HashMap<>(formFieldsMap.size());
            for (Entry<String, List<String>> entry: formFieldsMap.entrySet()) {
                map.put(entry.getKey(), entry.getValue().toArray(type));
            }
            return map;
        }
    }

    @Override
    public String getHeader(String name) {
        return httpServletRequest.getHeader(name);
    }

    @Override
    public List<String> getHeaders(String name) {
        return Collections.list(httpServletRequest.getHeaders(name));
    }

    @Override
    public Map<String, List<String>> getHeaders() {
        Map<String, List<String>> headers = new HashMap<>();
        Enumeration<String> names = httpServletRequest.getHeaderNames();
        while (names.hasMoreElements()) {
            String name = names.nextElement();
            headers.put(name, Collections.list(httpServletRequest.getHeaders(name)));
        }
        return headers;
    }

    @Override
    public Cookie getCookie(String cookieName) {
        javax.servlet.http.Cookie[] cookies = httpServletRequest.getCookies();

        if (cookies == null) {
            return null;
        }

        javax.servlet.http.Cookie servletCookie = ServletCookieHelper.getCookie(cookieName, cookies);

        if (servletCookie == null) {
            return null;
        }

        return ServletCookieHelper.convertServletCookieToNinjaCookie(servletCookie);
    }

    @Override
    public boolean hasCookie(String cookieName) {
        return ServletCookieHelper.getCookie(cookieName, httpServletRequest.getCookies()) != null;
    }

    @Override
    public List<Cookie> getCookies() {
        javax.servlet.http.Cookie[] servletCookies = httpServletRequest.getCookies();

        if (servletCookies == null) {
            return Collections.EMPTY_LIST;
        }

        List<Cookie> ninjaCookies = new ArrayList<>(servletCookies.length);

        for (javax.servlet.http.Cookie cookie : servletCookies) {
            Cookie ninjaCookie = ServletCookieHelper.convertServletCookieToNinjaCookie(cookie);
            ninjaCookies.add(ninjaCookie);
        }

        return ninjaCookies;
    }

    @Override
    public void addCookie(Cookie cookie) {
        httpServletResponse.addCookie(ServletCookieHelper.convertNinjaCookieToServletCookie(cookie));
    }

    @Override
    public boolean isAsync() {
        return asyncStrategy != null;
    }

    @Override
    public void returnResultAsync(Result result) {
        synchronized (asyncLock) {
            handleAsync();
            asyncStrategy.returnResultAsync(result, this);
        }
    }

    @Override
    public void handleAsync() {
        synchronized (asyncLock) {
            if (asyncStrategy == null) {
                asyncStrategy = AsyncStrategyFactoryHolder.getInstance(
                        httpServletRequest).createStrategy(httpServletRequest, resultHandler);
                asyncStrategy.handleAsync();
            }
        }
    }

    /**
     * Used to indicate that the controller has finished executing
     */
    @Override
    public Result controllerReturned() {
        if (asyncStrategy != null) {
            return asyncStrategy.controllerReturned();
        }
        return null;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return httpServletRequest.getInputStream();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return httpServletRequest.getReader();
    }

    @Override
    protected ResponseStreams finalizeHeaders(Result result, Boolean handleFlashAndSessionCookie) {
        // delegate cookie, session, and flash to parent
        super.finalizeHeaders(result, handleFlashAndSessionCookie);

        httpServletResponse.setStatus(result.getStatusCode());

        // copy headers
        for (Entry<String, String> header : result.getHeaders().entrySet()) {
            httpServletResponse.addHeader(header.getKey(), header.getValue());
        }

        // set content type
        if (result.getContentType() != null) {

            httpServletResponse.setContentType(result.getContentType());
        }

        // Set charset => use utf-8 if not set
        // Sets correct encoding for Content-Type. But also for the output
        // writers.
        if (result.getCharset() != null) {
            httpServletResponse.setCharacterEncoding(result.getCharset());
        } else {
            httpServletResponse.setCharacterEncoding(NinjaConstant.UTF_8);
        }

        // possibly
        ResponseStreamsServlet responseStreamsServlet = new ResponseStreamsServlet();
        responseStreamsServlet.init(httpServletResponse);

        return responseStreamsServlet;
    }

    @Override
    public String getRequestContentType() {
        return httpServletRequest.getContentType();
    }

    @Override
    public boolean isMultipart() {
        return ServletFileUpload.isMultipartContent(httpServletRequest);
    }

    @Override
    public FileItemIterator getFileItemIterator() {

        long maxFileSize = ninjaProperties.getIntegerWithDefault(NinjaConstant.UPLOADS_MAX_FILE_SIZE, -1);
        long maxTotalSize = ninjaProperties.getIntegerWithDefault(NinjaConstant.UPLOADS_MAX_TOTAL_SIZE, -1);

        ServletFileUpload upload = new ServletFileUpload();
        upload.setFileSizeMax(maxFileSize);
        upload.setSizeMax(maxTotalSize);

        FileItemIterator fileItemIterator = null;

        try {
            fileItemIterator = upload.getItemIterator(httpServletRequest);
        } catch (FileUploadException | IOException e) {
            logger.error("Error while trying to process mulitpart file upload",
                    e);
        }

        return fileItemIterator;
    }

    private String performGetRequestPath() {
        // http://stackoverflow.com/questions/966077/java-reading-undecoded-url-from-servlet

        // this one is unencoded:
        String unencodedContextPath = httpServletRequest.getContextPath();
        // this one is unencoded, too, but may containt the context:
        String fullUnencodedUri = httpServletRequest.getRequestURI();

        String result = fullUnencodedUri.substring(unencodedContextPath
                .length());

        return result;
    }

    /**
     * When a servlet engine gets a content type like: "application/json" it
     * assumes a default encoding of iso-xxxxx.
     *
     * That is not what Ninja does (and is not consistent with default encodings
     * of application/json and application/xml).
     *
     * Therefore we'll set utf-8 as request encoding if it is not set.
     */
    private void enforceCorrectEncodingOfRequest() {

        String charset = NinjaConstant.UTF_8;

        String contentType = getHeader(CONTENT_TYPE);

        if (contentType != null) {
            charset = HttpHeaderUtils.getCharsetOfContentTypeOrUtf8(contentType);
        }

        try {
            httpServletRequest.setCharacterEncoding(charset);
        } catch (UnsupportedEncodingException e) {
            logger.error("Server does not support charset of content type: " + contentType);
        }

    }

    /**
     * Convenience method to access ServletContext of this context.
     *
     * @return ServletContext of this Context
     */
    public ServletContext getServletContext() {
        return servletContext;
    }

    /**
     * Convenience method to access HttpServletRequest of this context.
     *
     * @return HttpServletRequest of this Context
     */
    public HttpServletRequest getHttpServletRequest() {
        return httpServletRequest;
    }

    /**
     * Convenience method to access HttpServletResponse of this context.
     *
     * Usually you don't want to do that.
     *
     * @return HttpServletResponse of this Context.
     */
    public HttpServletResponse getHttpServletResponse() {
        return httpServletResponse;
    }

    private void processFormFields() {
        if (formFieldsProcessed) return;
        formFieldsProcessed = true;

        // return if not multipart
        if (!ServletFileUpload.isMultipartContent(httpServletRequest))
            return;

        // get fileProvider from route method/class, or defaults to an injected one
        // if none injected, then we do not process form fields this way and let the user
        // call classic getFileItemIterator() by himself
        FileProvider fileProvider = null;
        if (route != null) {
            if (fileProvider == null) {
                fileProvider = route.getControllerMethod().getAnnotation(FileProvider.class);
            }
            if (fileProvider == null) {
                fileProvider = route.getControllerClass().getAnnotation(FileProvider.class);
            }
        }

        // get file item provider from file provider or default one
        FileItemProvider fileItemProvider = null;
        if (fileProvider == null) {
            fileItemProvider = injector.getInstance(FileItemProvider.class);
        } else {
            fileItemProvider = injector.getInstance(fileProvider.value());
        }

        if (fileItemProvider instanceof NoFileItemProvider) return;

        // Initialize maps and other constants
        ArrayListMultimap<String, String> formMap = ArrayListMultimap.create();
        ArrayListMultimap<String, FileItem> fileMap = ArrayListMultimap.create();

        // This is the iterator we can use to iterate over the contents of the request.
        try {

            FileItemIterator fileItemIterator = getFileItemIterator();

            while (fileItemIterator.hasNext()) {

                FileItemStream item = fileItemIterator.next();

                if (item.isFormField()) {

                    String charset = NinjaConstant.UTF_8;

                    String contentType = item.getContentType();

                    if (contentType != null) {
                        charset = HttpHeaderUtils.getCharsetOfContentTypeOrUtf8(contentType);
                    }

                    // save the form field for later use from getParameter
                    String value = Streams.asString(item.openStream(), charset);
                    formMap.put(item.getFieldName(), value);

                } else {

                    // process file as input stream and save for later use in getParameterAsFile or getParameterAsInputStream
                    FileItem fileItem = fileItemProvider.create(item);
                    fileMap.put(item.getFieldName(), fileItem);
                }
            }
        } catch (FileUploadException | IOException e) {
            throw new RuntimeException("Failed to parse multipart request data", e);
        }

        // convert both multimap<K,V> to map<K,List<V>>
        formFieldsMap = toUnmodifiableMap(formMap);
        fileFieldsMap = toUnmodifiableMap(fileMap);
    }

    /**
     * Utility method to convert a Guava Multimap to an unmodifiable Map that
     * uses a List<T> as a value. Optimized for the case where values are already
     * internally stored as a List<T> (e.g. ArrayListMultimap).
     * @param <T> The value type
     * @param multimap The multimap to convert from
     * @return The unmodifiable converted map
     */
    private <T> Map<String, List<T>> toUnmodifiableMap(Multimap<String, T> multimap) {
        Map<String, List<T>> map = new HashMap<>(multimap.size());

        for (Entry<String, Collection<T>> entry: multimap.asMap().entrySet()) {
            Collection<T> value = entry.getValue();
            if (value == null) {
                Collections.emptyList();
            } else if (value instanceof List) {
                map.put(entry.getKey(), (List<T>)value);
            } else {
                map.put(entry.getKey(), new ArrayList<>(value));
            }
        }

        return Collections.unmodifiableMap(map);
    }

    @Override
    public void cleanup() {
        // call cleanup on all file items
        if (fileFieldsMap != null) {
            for (List<FileItem> files: fileFieldsMap.values()) {
                for (FileItem file: files) {
                    file.cleanup();
                }
            }
        }
    }
}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import ninja.ControllerMethods.ControllerMethod;
import ninja.ReverseRouter.Builder;
import ninja.utils.LambdaRoute;
import ninja.utils.MethodReference;
import ninja.utils.NinjaProperties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;

/**
 * Reverse routing. Lookup the uri associated with a controller method.
 *
 * @author Joe Lauer (jjlauer)
 */
public class ReverseRouter implements WithControllerMethod<Builder> {
    static private final Logger log = LoggerFactory.getLogger(ReverseRouter.class);

    static public class Builder {

        private final String contextPath;
        private final Route route;
        private String scheme;
        private String hostname;
        private Map<String,String> pathParams;
        private Map<String,String> queryParams;

        public Builder(String contextPath, Route route) {
            this.contextPath = contextPath;
            this.route = route;
        }

        public Builder scheme(String scheme) {
            this.scheme = scheme;
            return this;
        }

        /**
         * Make this an absolute route by including the current scheme (e.g. http)
         * and hostname (e.g. www.example.com).
         * @param scheme The scheme such as "http" or "https"
         * @param hostname The hostname such as "www.example.com" or "www.example.com:8080"
         * @return This builder
         */
        public Builder absolute(String scheme, String hostname) {
            this.scheme = scheme;
            this.hostname = hostname;
            return this;
        }

        /**
         * Make this an absolute route by including the current scheme (e.g. http)
         * and hostname (e.g. www.example.com).  If the route is to a websocket
         * then this will then return "ws" or "wss" if TLS is detected.
         * @param context The current context
         * @return This builder
         */
        public Builder absolute(Context context) {
            String s = context.getScheme();
            String h = context.getHostname();

            if (this.route.isHttpMethodWebSocket()) {
                if ("https".equalsIgnoreCase(s)) {
                    s = "wss";
                } else {
                    s = "ws";
                }
            }

            return this.absolute(s, h);
        }

        public Route getRoute() {
            return route;
        }

        public Map<String, String> getPathParams() {
            return pathParams;
        }

        public Map<String, String> getQueryParams() {
            return queryParams;
        }

        /**
         * Add a parameter as a path replacement. Will validate the path parameter
         * exists. This method will URL encode the values when building the final
         * result.
         * @param name The path parameter name
         * @param value The path parameter value
         * @return A reference to this builder
         * @see #rawPathParam(java.lang.String, java.lang.Object)
         */
        public Builder pathParam(String name, Object value) {
            return setPathParam(name, value, false);
        }

        /**
         * Identical to path except the path parameter value will
         * NOT be url encoded when building the final url.
         * @param name The path parameter name
         * @param value The path parameter value
         * @return A reference to this builder
         * @see #pathParam(java.lang.String, java.lang.Object)
         */
        public Builder rawPathParam(String name, Object value) {
            return setPathParam(name, value, true);
        }

        private Builder setPathParam(String name, Object value, boolean raw) {
            Objects.requireNonNull(name, "name required");
            Objects.requireNonNull(value, "value required");

            if (route.getParameters() == null || !route.getParameters().containsKey(name)) {
                throw new IllegalArgumentException("Reverse route " + route.getUri()
                    + " does not have a path parameter '" + name + "'");
            }

            if (this.pathParams == null) {
                this.pathParams = new LinkedHashMap<>();
            }

            this.pathParams.put(name, safeValue(value, raw));

            return this;
        }

        /**
         * Add a parameter as a queryParam string value. This method will URL encode
         * the values when building the final result.
         * @param name The queryParam string parameter name
         * @param value The queryParam string parameter value
         * @return A reference to this builder
         * @see #rawQueryParam(java.lang.String, java.lang.Object)
         */
        public Builder queryParam(String name, Object value) {
            return setQueryParam(name, value, false);
        }

        /**
         * Identical to queryParam except the queryParam string value will
         * NOT be url encoded when building the final url.
         * @param name The queryParam string parameter name
         * @param value The queryParam string parameter value
         * @return A reference to this builder
         * @see #queryParam(java.lang.String, java.lang.Object)
         */
        public Builder rawQueryParam(String name, Object value) {
            return setQueryParam(name, value, true);
        }

        private Builder setQueryParam(String name, Object value, boolean raw) {
            Objects.requireNonNull(name, "name required");

            if (this.queryParams == null) {
                // retain ordering
                this.queryParams = new LinkedHashMap<>();
            }

            this.queryParams.put(name, safeValue(value, raw));

            return this;
        }

        private String safeValue(Object value, boolean raw) {
            String s = (value == null ? null : value.toString());
            if (!raw && s != null) {
                try {
                    s = URLEncoder.encode(s, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    throw new IllegalArgumentException(e);
                }
            }
            return s;
        }

        private int safeMapSize(Map map) {
            return (map != null ? map.size() : 0);
        }

        /**
         * Builds the final url. Will validate expected parameters match actual.
         * @return The final resulting url
         */
        public String build() {
            // number of pathOrQueryParams valid?
            int expectedParamSize = safeMapSize(this.route.getParameters());
            int actualParamSize = safeMapSize(this.pathParams);
            if (expectedParamSize != actualParamSize) {
                throw new IllegalArgumentException("Reverse route " + route.getUri()
                    + " requires " + expectedParamSize + " parameters but got "
                    + actualParamSize + " instead");
            }

            String rawUri = this.route.getUri();

            StringBuilder buffer = new StringBuilder(rawUri.length());

            // append scheme + hostname?
            if (this.scheme != null && this.hostname != null) {
                buffer.append(this.scheme);
                buffer.append("://");
                buffer.append(this.hostname);
            }

            // append contextPath
            if (this.contextPath != null && this.contextPath.length() > 0) {
                buffer.append(this.contextPath);
            }

            // replace path parameters
            int lastIndex = 0;

            if (this.pathParams != null) {
                for (RouteParameter rp : this.route.getParameters().values()) {
                    String value = this.pathParams.get(rp.getName());

                    if (value == null) {
                        throw new IllegalArgumentException("Reverse route " + route.getUri()
                            + " missing value for path parameter '" + rp.getName() + "'");
                    }

                    // append any text before this token
                    buffer.append(rawUri.substring(lastIndex, rp.getIndex()));
                    // append value
                    buffer.append(value);
                    // the next index to start from
                    lastIndex = rp.getIndex() + rp.getToken().length();
                }
            }

            // append whatever remains
            if (lastIndex < rawUri.length()) {
                buffer.append(rawUri.substring(lastIndex));
            }

            // append queryParam pathOrQueryParams
            if (this.queryParams != null) {
                int i = 0;
                for (Map.Entry<String,String> entry : this.queryParams.entrySet()) {
                    buffer.append((i == 0 ? '?' : '&'));
                    buffer.append(entry.getKey());
                    if (entry.getValue() != null) {
                        buffer.append('=');
                        buffer.append(entry.getValue());
                    }
                    i++;
                }
            }

            return buffer.toString();
        }

        /**
         * Builds the result as a ninja.Result redirect.
         * @return A Ninja redirect result
         */
        public Result redirect() {
            return Results.redirect(build());
        }

        @Override
        public String toString() {
            return this.build();
        }
    }

    private final NinjaProperties ninjaProperties;
    private final Router router;

    @Inject
    public ReverseRouter(NinjaProperties ninjaProperties,
                         Router router) {
        this.ninjaProperties = ninjaProperties;
        this.router = router;
    }

    /**
     * Retrieves a the reverse route for this controllerClass and method.
     *
     * @param controllerClass The controllerClass e.g. ApplicationController.class
     * @param methodName the methodName of the class e.g. "index"
     * @return A Builder allowing setting path placeholders and
      queryParam string parameters.
     */
    public Builder with(Class<?> controllerClass, String methodName) {
        return builder(controllerClass, methodName);
    }

    /**
     * Retrieves a the reverse route for the method reference (e.g. controller
     * class and method name).
     *
     * @param methodRef The reference to a method
     * @return A Builder allowing setting path placeholders and
      queryParam string parameters.
     */
    public Builder with(MethodReference methodRef) {
        return builder(methodRef.getDeclaringClass(), methodRef.getMethodName());
    }

    /**
     * Retrieves a the reverse route for a method referenced with Java-8
     * lambdas (functional method references).
     *
     * @param controllerMethod The Java-8 style method reference such as
     *      ApplicationController::index.
     * @return A Builder allowing setting path placeholders and
      queryParam string parameters.
     */
    @Override
    public Builder with(ControllerMethod controllerMethod) {
        LambdaRoute lambdaRoute = LambdaRoute.resolve(controllerMethod);

        // only need the functional method for the reverse lookup
        Method method = lambdaRoute.getFunctionalMethod();

        return builder(method.getDeclaringClass(), method.getName());
    }

    private Builder builder(Class<?> controllerClass, String methodName) {
        Optional<Route> route = this.router.getRouteForControllerClassAndMethod(
            controllerClass, methodName);

        if (route.isPresent()) {
            return new Builder(this.ninjaProperties.getContextPath(), route.get());
        }

        throw new IllegalArgumentException("Reverse route not found for " +
            controllerClass.getCanonicalName() + "." + methodName);
    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.servlet;

import com.google.inject.AbstractModule;
import ninja.Bootstrap;
import javax.servlet.ServletContextEvent;
import ninja.utils.NinjaModeHelper;
import ninja.utils.NinjaPropertiesImpl;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.servlet.GuiceServletContextListener;
import java.util.Optional;
import javax.servlet.ServletContext;
import javax.websocket.server.ServerContainer;
import ninja.utils.SwissKnife;
import ninja.websockets.WebSockets;
import ninja.websockets.jsr356.Jsr356WebSockets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * define in web.xml:
 *
 * <listener>
 *   <listener-class>ninja.NinjaServletListener</listener-class>
 * </listener>
 *
 * @author zoza
 *
 */
public class NinjaServletListener extends GuiceServletContextListener {
    private static final Logger logger = LoggerFactory.getLogger(NinjaServletListener.class);

    private volatile Bootstrap ninjaBootstrap;
    private NinjaPropertiesImpl ninjaProperties = null;
    private String contextPath;
    private Optional<Module> webSocketModule = Optional.empty();

    public synchronized void setNinjaProperties(NinjaPropertiesImpl ninjaPropertiesImpl) {
        if (this.ninjaProperties != null) {
            throw new IllegalStateException("NinjaProperties already set.");
        }
        this.ninjaProperties = ninjaPropertiesImpl;
    }

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        contextPath = servletContextEvent.getServletContext().getContextPath();

        // websocket enabled servlet containers populate this attribute with JSR 356
        // we save it here so we can inject it later into guice
        if (SwissKnife.doesClassExist(
                WebsocketGuiceModuleCreator.WEBSOCKET_SERVER_CONTAINER_CLASSNAME,
                this)) {

            this.webSocketModule = WebsocketGuiceModuleCreator.getWebsocketServerContainerIfPossible(
                    servletContextEvent.getServletContext());
        }

        super.contextInitialized(servletContextEvent);
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        ninjaBootstrap.shutdown();
        super.contextDestroyed(servletContextEvent);
    }

    /**
     * Only available after context initialization attempted.
     */
    public Bootstrap getNinjaBootstrap() {
        return this.ninjaBootstrap;
    }

    /**
     * Getting the injector is done via double locking in conjuction
     * with volatile keyword for thread safety.
     * See also: http://en.wikipedia.org/wiki/Double-checked_locking
     *
     * @return The injector for this application.
     */
    @Override
    public Injector getInjector() {

        // fetch instance variable into method, so that we access the volatile
        // global variable only once - that's better performance wise.
        Bootstrap ninjaBootstrapLocal = ninjaBootstrap;

        if (ninjaBootstrapLocal == null) {

            synchronized(this) {

                ninjaBootstrapLocal = ninjaBootstrap;

                if (ninjaBootstrapLocal == null) {

                    // if properties
                    if (ninjaProperties == null) {

                        ninjaProperties
                                = new NinjaPropertiesImpl(
                                        NinjaModeHelper.determineModeFromSystemPropertiesOrProdIfNotSet());

                    }

                    ninjaBootstrap
                            = createNinjaBootstrap(ninjaProperties, contextPath);
                    ninjaBootstrapLocal = ninjaBootstrap;

                }

            }

        }

        return ninjaBootstrapLocal.getInjector();

    }

    private Bootstrap createNinjaBootstrap(
            NinjaPropertiesImpl ninjaProperties,
            String contextPath) {

        // we set the contextpath.
        ninjaProperties.setContextPath(contextPath);

        ninjaBootstrap = new NinjaServletBootstrap(ninjaProperties);

        // if websocket container present then enable jsr-356 websockets
        webSocketModule.ifPresent(module -> {
            ninjaBootstrap.addModule(module);
        });

        ninjaBootstrap.boot();

        return ninjaBootstrap;
    }

    /**
     * Huh. Why is there a separate class like this. Couldn't we not just do
     * everything directly in NinjaServletListener? No.
     *
     * Problem:
     * When we'd use javax.websocket.server.ServerContainer inside
     * NinjaServletListener, then some ServletContainers will explode with a
     * ClassNotFoundException. For instance the Jetty bundled with the AppEngine
     * explodes like that.
     *
     * Solution:
     * NinjaServletListener checks if the class exists on the
     * classpath and only then calls this class/method that then causes the
     * classloader to actually load it.
     */
    static class WebsocketGuiceModuleCreator {

        public static final String WEBSOCKET_SERVER_CONTAINER_CLASSNAME
                = "javax.websocket.server.ServerContainer";

        public static Optional<Module> getWebsocketServerContainerIfPossible(
                ServletContext servletContext) {

            ServerContainer websocketServerContainer
                    = (ServerContainer) servletContext.getAttribute(WEBSOCKET_SERVER_CONTAINER_CLASSNAME);

            logger.info(
                "Using JSR-356 websocket container {}",
                websocketServerContainer
            );

            if (websocketServerContainer != null) {
                Module websocketsModule = new AbstractModule() {
                    @Override
                    protected void configure() {
                        bind(ServerContainer.class).toInstance(websocketServerContainer);
                        bind(WebSockets.class).to(Jsr356WebSockets.class);
                    }
                };
                return Optional.of(websocketsModule);

            } else {
                return Optional.empty();
            }
        }

    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

import ninja.utils.HttpCacheToolkit;
import ninja.utils.MimeTypes;
import ninja.utils.NinjaProperties;
import ninja.utils.ResponseStreams;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.io.ByteStreams;
import com.google.inject.Inject;
import com.google.inject.Singleton;

/**
 * This controller serves public resources under /assets.
 * It is capable of serving static files, webjars or files from a completely
 * different directory on the server.
 */
@Singleton
public class AssetsController {

    private final static Logger logger = LoggerFactory
            .getLogger(AssetsController.class);

    public final static String ASSETS_DIR = "assets";

    public final static String FILENAME_PATH_PARAM = "fileName";

    private final MimeTypes mimeTypes;

    private final HttpCacheToolkit httpCacheToolkit;

    private final NinjaProperties ninjaProperties;

    private final AssetsControllerHelper assetsControllerHelper;

    @Inject
    public AssetsController(AssetsControllerHelper assetsControllerHelper,
                            HttpCacheToolkit httpCacheToolkit,
                            MimeTypes mimeTypes,
                            NinjaProperties ninjaProperties) {
        this.assetsControllerHelper = assetsControllerHelper;
        this.httpCacheToolkit = httpCacheToolkit;
        this.mimeTypes = mimeTypes;
        this.ninjaProperties = ninjaProperties;
    }

    /**
     * Serves resources from the assets directory of your application.
     *
     * For instance:
     * route: /robots.txt
     * A request to /robots.txt will be served from /assets/robots.txt.
     *
     * You can also use a path like the following to serve files:
     * route: /assets/{fileName: .*}
     *
     * matches
     * /assets/app/app.css
     * and will return
     * /assets/app/app.css (from your jar).
     *
     */
    public Result serveStatic() {
        Object renderable = new Renderable() {
            @Override
            public void render(Context context, Result result) {
                String fileName = getFileNameFromPathOrReturnRequestPath(context);
                URL url = getStaticFileFromAssetsDir(fileName);
                streamOutUrlEntity(url, context, result);
            }
        };
        return Results.ok().render(renderable);
    }

    /**
     * Serves resources from the assets directory of your application.
     *
     * For instance:
     * A request to /robots.txt will be served from /assets/robots.txt.
     * Request to /public/css/app.css will be served from /assets/css/app.css.
     *
     */
    public Result serveWebJars() {
        Object renderable = new Renderable() {
            @Override
            public void render(Context context, Result result) {
                String fileName = getFileNameFromPathOrReturnRequestPath(context);
                URL url = getStaticFileFromMetaInfResourcesDir(fileName);
                streamOutUrlEntity(url, context, result);
            }
        };
        return Results.ok().render(renderable);
    }

    private void streamOutUrlEntity(URL url, Context context, Result result) {
        // check if stream exists. if not print a notfound exception
        if (url == null) {
            context.finalizeHeadersWithoutFlashAndSessionCookie(Results.notFound());
        } else if (assetsControllerHelper.isDirectoryURL(url)) {
            // Disable listing of directory contents
            context.finalizeHeadersWithoutFlashAndSessionCookie(Results.notFound());
        } else {
            try {
                URLConnection urlConnection = url.openConnection();
                Long lastModified = urlConnection.getLastModified();
                httpCacheToolkit.addEtag(context, result, lastModified);

                if (result.getStatusCode() == Result.SC_304_NOT_MODIFIED) {
                    // Do not stream anything out. Simply return 304
                    context.finalizeHeadersWithoutFlashAndSessionCookie(result);
                } else {
                    result.status(200);

                    // Try to set the mimetype:
                    String mimeType = mimeTypes.getContentType(context,
                            url.getFile());

                    if (mimeType != null && !mimeType.isEmpty()) {
                        result.contentType(mimeType);
                    }

                    ResponseStreams responseStreams = context
                            .finalizeHeadersWithoutFlashAndSessionCookie(result);

                    try (InputStream inputStream = urlConnection.getInputStream();
                        OutputStream outputStream = responseStreams.getOutputStream()) {
                        ByteStreams.copy(inputStream, outputStream);
                    }

                }

            } catch (IOException e) {
                logger.error("error streaming file", e);
            }

        }

    }

    /**
     * Loads files from assets directory. This is the default directory
     * of Ninja where to store stuff. Usually in src/main/java/assets/.
     */
    private URL getStaticFileFromAssetsDir(String fileName) {

        URL url = null;

        if (ninjaProperties.isDev()
                // Testing that the file exists is important because
                // on some dev environments we do not get the correct asset dir
                // via System.getPropery("user.dir").
                // In that case we fall back to trying to load from classpath
                && new File(assetsDirInDevModeWithoutTrailingSlash()).exists()) {
            String finalNameWithoutLeadingSlash = assetsControllerHelper.normalizePathWithoutLeadingSlash(fileName, false);
            File possibleFile = new File(
                    assetsDirInDevModeWithoutTrailingSlash()
                            + File.separator
                            + finalNameWithoutLeadingSlash);
            url = getUrlForFile(possibleFile);
        } else {
            String finalNameWithoutLeadingSlash = assetsControllerHelper.normalizePathWithoutLeadingSlash(fileName, true);
            url = this.getClass().getClassLoader()
                    .getResource(ASSETS_DIR
                                 + "/"
                                 + finalNameWithoutLeadingSlash);
        }

        return url;
    }

    private URL getUrlForFile(File possibleFileInSrc) {
        if (possibleFileInSrc.exists() && !possibleFileInSrc.isDirectory()) {
          try {
            return possibleFileInSrc.toURI().toURL();
          } catch(MalformedURLException malformedURLException) {
            logger.error("Error in dev mode while streaming files from src dir. ", malformedURLException);
          }
        }
        return null;
    }

    /**
     * Loads files from META-INF/resources directory.
     * This is compatible with Servlet 3.0 specification and allows
     * to use e.g. webjars project.
     *
     */
    private URL getStaticFileFromMetaInfResourcesDir(String fileName) {
        String finalNameWithoutLeadingSlash
                = assetsControllerHelper.normalizePathWithoutLeadingSlash(fileName, true);
        URL url = null;
        url = this.getClass().getClassLoader().getResource("META-INF/resources/webjars/" + finalNameWithoutLeadingSlash);
        return url;
    }

    private static String getFileNameFromPathOrReturnRequestPath(Context context) {

        String fileName = context.getPathParameter(FILENAME_PATH_PARAM);

        if (fileName == null) {
            fileName = context.getRequestPath();
        }
        return fileName;

    }

    /**
     * Used in dev mode: Streaming directly from src dir without jetty reload.
     */
    private String assetsDirInDevModeWithoutTrailingSlash() {
        String srcDir  = System.
                getProperty("user.dir")
                + File.separator
                + "src"
                + File.separator
                + "main"
                + File.separator
                + "java";
        return srcDir + File.separator + ASSETS_DIR;
    }
}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.template;

import java.io.IOException;
import java.io.OutputStream;
import java.util.regex.Pattern;

import ninja.Context;
import ninja.Result;
import ninja.utils.NinjaConstant;
import ninja.utils.NinjaProperties;
import ninja.utils.ResponseStreams;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.JSONPObject;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import com.google.inject.Singleton;

/**
 * JSONP engine. Outputs the given result as JSONP output: Javascript callback
 * with data as a parameter. Contains JSONP validation regular expression to
 * test the validness of the JSONP callback. The valid callbacks are simple
 * functions like myJsFunction or simple object path looking like:
 * something1.something2.something3 .
 * See {@linkplain ninja.template.TemplateEngineJsonPValidatorTest} for supported cases tests.
 * This is a subset of rules from the
 * following article:
 *
 * @see Sanitizing JSONP callbacks
 */
@Singleton
public class TemplateEngineJsonP implements TemplateEngine {

    private final Logger logger = LoggerFactory.getLogger(TemplateEngineJsonP.class);

    static final String DEFAULT_CALLBACK_PARAMETER_NAME = "callback";

    static final String DEFAULT_CALLBACK_PARAMETER_VALUE = "onResponse";

    static final Pattern CALLBACK_SECURITY_VALIDATION_REGEXP
            = Pattern.compile("^([a-zA-Z$_]{1}[a-zA-Z0-9$_.]*[a-zA-Z0-9$_]{1}){1,}$");

    private final ObjectMapper objectMapper;

    private final String callbackParameterName;

    @Inject
    public TemplateEngineJsonP(ObjectMapper objectMapper, NinjaProperties properties) {

        this.objectMapper = objectMapper;
        this.callbackParameterName = properties.getWithDefault(
                NinjaConstant.NINJA_JSONP_CALLBACK_PARAMETER,
                DEFAULT_CALLBACK_PARAMETER_NAME);
    }

    @Override
    public void invoke(Context context, Result result) {
        ResponseStreams responseStreams = context.finalizeHeaders(result);
        String callback = getCallbackName(context);
        try (OutputStream outputStream = responseStreams.getOutputStream()) {
            objectMapper.writeValue(outputStream, new JSONPObject(callback, result.getRenderable()));
        } catch (IOException e) {
            logger.error("Error while rendering jsonp.", e);
        }
    }

    @Override
    public String getContentType() {
        return Result.APPLICATION_JSONP;
    }

    @Override
    public String getSuffixOfTemplatingEngine() {
        // intentionally returns null...
        return null;
    }

    private String getCallbackName(Context context) {
        String callback = context.getParameter(this.callbackParameterName, DEFAULT_CALLBACK_PARAMETER_VALUE);
        return isThisASecureCallbackName(callback) ? callback : DEFAULT_CALLBACK_PARAMETER_VALUE;
    }

    /**
     * Tests whether the given function name is a valid JSONP function
     * name/path.
     *
     * @param callback Callback value to test.
     * @return Whether the given function name is a valid JSONP function
     * name/path.
     */
    public static boolean isThisASecureCallbackName(String callback) {
        return !Strings.isNullOrEmpty(callback)
                && !callback.contains("..")
                && CALLBACK_SECURITY_VALIDATION_REGEXP.matcher(callback).matches();
    }
}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.i18n;

import java.text.MessageFormat;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

import ninja.Context;
import ninja.Result;
import ninja.utils.NinjaConstant;
import ninja.utils.NinjaProperties;
import ninja.utils.SwissKnife;

import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationConverter;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;

@Singleton
public class MessagesImpl implements Messages {

    private static Logger logger = LoggerFactory.getLogger(MessagesImpl.class);

    private final Map<String, Configuration> langToKeyAndValuesMapping;

    private final NinjaProperties ninjaProperties;

    private final Lang lang;

    @Inject
    public MessagesImpl(NinjaProperties ninjaProperties,
                        Lang lang) {
        this.ninjaProperties = ninjaProperties;
        this.lang = lang;
        this.langToKeyAndValuesMapping = loadAllMessageFilesForRegisteredLanguages();
    }

    @Override
    public Optional<String> get(String key,
                      Context context,
                      Optional<Result> result,
                      Object... parameter) {
        Optional<String> language = lang.getLanguage(context, result);
        return get(key, language, parameter);
    }

    @Override
    public Optional<String> get(String key, Optional<String> language, Object... params) {

        Configuration configuration = getLanguageConfigurationForLocale(language);
        String value = configuration.getString(key);

        if (value != null) {
            MessageFormat messageFormat = getMessageFormatForLocale(value, language);
            return Optional.of(messageFormat.format(params));
        } else {
            return Optional.empty();
        }
    }

    @Override
    public Map<Object, Object> getAll(Context context, Optional<Result> result) {

        Optional<String> language = lang.getLanguage(context, result);
        return getAll(language);

    }

    @Override
    public Map<Object, Object> getAll(Optional<String> language) {

        Configuration configuration = getLanguageConfigurationForLocale(language);

        return ConfigurationConverter.getMap(configuration);

    }

    @Override
    public String getWithDefault(String key,
                                 String defaultMessage,
                                 Context context,
                                 Optional<Result> result,
                                 Object... params) {

        Optional<String> language = lang.getLanguage(context, result);

        return getWithDefault(key, defaultMessage, language, params);

    }

    @Override
    public String getWithDefault(String key,
                                 String defaultMessage,
                                 Optional<String> language,
                                 Object... params) {
        Optional<String> value = get(key, language, params);
        if (value.isPresent()) {
            return value.get();
        } else {
            MessageFormat messageFormat = getMessageFormatForLocale(defaultMessage, language);
            return messageFormat.format(params);
        }
    }

    /**
     * Attempts to load a message file and sets the file changed reloading
     * strategy on the configuration if the runtime mode is Dev.
     */
    private PropertiesConfiguration loadLanguageConfiguration(String fileOrUrl) {
        PropertiesConfiguration configuration = SwissKnife
                .loadConfigurationInUtf8(fileOrUrl);

        if (configuration != null && ninjaProperties.isDev()) {
            // enable runtime reloading of translations in dev mode
            FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
            configuration.setReloadingStrategy(strategy);
        }

        return configuration;
    }

    /**
     * Does all the loading of message files.
     *
     * Only registered messages in application.conf are loaded.
     *
     */
    private  Map<String, Configuration> loadAllMessageFilesForRegisteredLanguages() {

        Map<String, Configuration> langToKeyAndValuesMappingMutable = Maps.newHashMap();

        // Load default messages:
        Configuration defaultLanguage = loadLanguageConfiguration("conf/messages.properties");

        // Make sure we got the file.
        // Everything else does not make much sense.
        if (defaultLanguage == null) {
            throw new RuntimeException(
                    "Did not find conf/messages.properties. Please add a default language file.");
        } else {
            langToKeyAndValuesMappingMutable.put("", defaultLanguage);
        }

        // Get the languages from the application configuration.
        String[] applicationLangs = ninjaProperties
                .getStringArray(NinjaConstant.applicationLanguages);

        // If we don't have any languages declared we just return.
        // We'll use the default messages.properties file.
        if (applicationLangs == null) {
            return ImmutableMap.copyOf(langToKeyAndValuesMappingMutable);
        }

        // Load each language into the HashMap containing the languages:
        for (String lang : applicationLangs) {

            // First step: Load complete language eg. en-US
            Configuration configuration = loadLanguageConfiguration(String
                    .format("conf/messages_%s.properties", lang));

            Configuration configurationLangOnly = null;

            // If the language has a country code load the default values for
            // the language, too. For instance missing variables in en-US will
            // be
            // Overwritten by the default languages.
            if (lang.contains("-")) {
                // get the lang
                String langOnly = lang.split("-")[0];

                // And load the configuraion
                configurationLangOnly = loadLanguageConfiguration(String
                        .format("conf/messages_%s.properties", langOnly));

            }

            // This is strange. If you defined the language in application.conf
            // it should be there propably.
            if (configuration == null) {
                logger.info(
                        "Did not find conf/messages_{}.properties but it was specified in application.conf. Using default language instead.",
                        lang);

            } else {

                // add new language, but combine with default language if stuff
                // is missing...
                CompositeConfiguration compositeConfiguration = new CompositeConfiguration();
                // Add eg. "en-US"
                compositeConfiguration.addConfiguration(configuration);

                // Add eg. "en"
                if (configurationLangOnly != null) {
                    compositeConfiguration
                            .addConfiguration(configurationLangOnly);
                }
                // Add messages.conf (default pack)
                compositeConfiguration.addConfiguration(defaultLanguage);

                // and add the composed configuration to the hashmap with the
                // mapping.
                langToKeyAndValuesMappingMutable.put(lang,
                        (Configuration) compositeConfiguration);
            }

        }

        return ImmutableMap.copyOf(langToKeyAndValuesMappingMutable);

    }

    /**
     * Retrieves the matching language file from an arbitrary one or two part
     * locale String ("en-US", or "en" or "de"...).
     * <p>
     *
     * @param language
     *            A two or one letter language code such as "en-US" or "en" or
     *            "en-US,en;q=0.8,de;q=0.6".
     * @return The matching configuration from the hashmap. Or the default
     *         mapping if no one has been found.
     */
    private Configuration getLanguageConfigurationForLocale(Optional<String> language) {

        // if language is null we return the default language.
        if (!language.isPresent()) {
            return langToKeyAndValuesMapping.get("");
        }

        // Check if we get a registered mapping for the language input string.
        // At that point the language may be either language-country or only country.
        // extract multiple languages from Accept-Language header
        String[] languages = language.get().split(",");
        for (String l: languages){
            l = l.trim();
            // Ignore the relative quality factor in Accept-Language header
            if (l.contains(";")){
                l = l.split(";")[0];
            }
            Configuration configuration = langToKeyAndValuesMapping.get(l);
            if (configuration != null) {
                return configuration;
            }

            // If we got a two part language code like "en-US" we split it and
            // search only for the language "en".
            if (l.contains("-")) {
                String[] array = l.split("-");
                String languageWithoutCountry = array[0];
                // Modify country code to upper case for IE and Firefox
                if(array.length > 1){
                    String country = array[1];
                    String languageWithUpperCaseCountry = languageWithoutCountry + "-" + country.toUpperCase();
                    configuration = langToKeyAndValuesMapping.get(languageWithUpperCaseCountry);
                    if (configuration != null) {
                        return configuration;
                    }
                }
                configuration = langToKeyAndValuesMapping
                        .get(languageWithoutCountry);

                if (configuration != null) {

                    return configuration;
                }

            }
        }

        // Oops. Nothing found. We return the default language (by convention guaranteed to work).
        return langToKeyAndValuesMapping.get("");

    }

    MessageFormat getMessageFormatForLocale(String value, Optional<String> language) {
        Locale locale = lang.getLocaleFromStringOrDefault(language);
        MessageFormat messageFormat = new MessageFormat(value, locale);
        return messageFormat;
    }
}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.logging;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;

import ninja.utils.NinjaProperties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;

import com.google.common.io.Resources;

/**
 *
 * Helps to configure Logback from application.conf via a property named
 * "logback.configurationFile. Allows to load arbitrary configurations that fit
 * your application.
 *
 * @author ra
 */
public class LogbackConfigurator {

    public final static String LOGBACK_CONFIGURATION_FILE_PROPERTY = "logback.configurationFile";

    private final static Logger logger = LoggerFactory.getLogger(LogbackConfigurator.class);

    public static void initConfiguration(NinjaProperties ninjaProperties) {

        // First of all check if someone set
        // -Dlogback.configurationFile=logback_prod.xml
        // This is a default property of Logback (http://logback.qos.ch/manual/configuration.html).
        // If that is the case we do nothing and leave everything to LogBack
        if (System.getProperty(LOGBACK_CONFIGURATION_FILE_PROPERTY) != null) {
            return;
        }

        // If not we check if we got a logback configurationFile declared
        // in our application.conf and load it...
        String logbackConfigurationFile
                = ninjaProperties.get(LOGBACK_CONFIGURATION_FILE_PROPERTY);

        if (logbackConfigurationFile == null) {
            return;
        }

        URL logbackConfigurationFileAsURL
            = getUrlForStringFromClasspathAsFileOrUrl(logbackConfigurationFile);

        if (logbackConfigurationFileAsURL == null) {
            logger.error("Cannot configure logger from {} provided in application.conf", logbackConfigurationFile);
            return;
        }

        // At that point we got a valid Url for configuring Logback. Let's do it :)

        // assume SLF4J is bound to logback in the current environment
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

        try {

            JoranConfigurator configurator = new JoranConfigurator();
            configurator.setContext(context);
            context.reset();
            configurator.doConfigure(logbackConfigurationFileAsURL);

        } catch (JoranException je) {
            // StatusPrinter will handle this
        }

        logger.info("Successfully configured application logging from: {}", logbackConfigurationFileAsURL);
        StatusPrinter.printInCaseOfErrorsOrWarnings(context);

    }

    /**
     * Looks up a potential file from
     * 1) The classpahth
     * 2) From the filesystem
     * 3) From an URL
     *
     * @param logbackConfigurationFile
     * @return null if not found or a valid url created from the logbackConfigurationFile
     */
    protected static URL getUrlForStringFromClasspathAsFileOrUrl(String logbackConfigurationFile) {

        URL url = null;

        try {
            url = Resources.getResource(logbackConfigurationFile);
        } catch (IllegalArgumentException ex) {
            // doing nothing intentionally..
        }

        if (url == null) {
            // configuring from file:
            try {
                File file = new File(logbackConfigurationFile);

                if (file.exists()) {
                    url = new File(logbackConfigurationFile).toURI().toURL();
                }

            } catch (MalformedURLException ex) {
                // doing nothing intentionally..
            }

        }

        if (url == null) {
            try {
                // we assume we got a real http://... url here...
                url = new URL(logbackConfigurationFile);
            } catch (MalformedURLException ex) {
                // doing nothing intentionally..
            }

        }

        return url;
    }
}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.i18n;

import java.util.Locale;
import java.util.Optional;

import ninja.Context;
import ninja.Cookie;
import ninja.Result;
import ninja.utils.NinjaConstant;
import ninja.utils.NinjaProperties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Splitter;
import com.google.inject.Inject;
import com.google.inject.Singleton;

@Singleton
public class LangImpl implements Lang {

    private static Logger logger = LoggerFactory.getLogger(LangImpl.class);

    private final String applicationCookiePrefix;

    private final int TEN_YEARS = 60*60*24*365*10;

    private final NinjaProperties ninjaProperties;

    private final String DEFAULT_LANGUAGE;

    @Inject
    public LangImpl(NinjaProperties ninjaProperties) {

        this.ninjaProperties = ninjaProperties;

        this.applicationCookiePrefix = ninjaProperties
                .getOrDie(NinjaConstant.applicationCookiePrefix);

        this.DEFAULT_LANGUAGE = getDefaultLanguage(this.ninjaProperties);

    }

    @Override
    public void clearLanguage(Result result) {

        Cookie defaultLangCookie = generateNinjaLanguageCookie();
        result.unsetCookie(defaultLangCookie.getName());

    }

    @Override
    public Result setLanguage(String locale, Result result) {

        Cookie defaultLangCookie = generateNinjaLanguageCookie();
        Cookie cookie = Cookie.builder(defaultLangCookie).setValue(locale).build();
        result.addCookie(cookie);

        return result;

    }

    @Override
    public Optional<String> getLanguage(Context context, Optional<Result> result) {

        Cookie defaultCookie = generateNinjaLanguageCookie();

        // Step 1: Determine language from result.
        // Result always has priority over context and will overwrite context.
        if (result.isPresent()) {
            Cookie cookie = result.get().getCookie(defaultCookie.getName());

            if (cookie != null) {

                if (cookie.getValue() != null
                        && !cookie.getValue().isEmpty()) {

                    //forced language is:
                    return Optional.of(cookie.getValue());
                }

            }
        }

        // Step 2 => we did not find the language in the result
        // We try to determine it from the context.
        Cookie cookie = context.getCookie(defaultCookie.getName());

        if (cookie != null) {

            if (cookie.getValue() != null
                    && !cookie.getValue().isEmpty()) {
                //forced language is:
                return Optional.of(cookie.getValue());
            }

        }

        // Step 3: Determine language from Accept-Language header.
        String acceptLanguage = context.getAcceptLanguage();
        if (acceptLanguage == null) {
            return Optional.empty();
        }

        // Check if we get a registered mapping for the language input string.
        // At that point the language may be either language-country or only country.
        // extract multiple languages from Accept-Language header
        Iterable<String> languages = Splitter.on(",").trimResults().split(acceptLanguage);

        for (String language: languages){
            // Ignore the relative quality factor in Accept-Language header
            if (language.contains(";")){
                language = language.split(";")[0];

                return Optional.of(language);

            } else {

                return Optional.of(language);

            }

        }

        return Optional.empty();

    }

    /**
     * Retrieves the language cookie or generates one with a very long max age (ten years).
     *
     * @param context
     * @return The cookie
     */
    private Cookie generateNinjaLanguageCookie() {

        Cookie cookie = Cookie.builder(applicationCookiePrefix
                    + ninja.utils.NinjaConstant.LANG_COOKIE_SUFFIX, "")
                    .setMaxAge(TEN_YEARS).build();

        return cookie;

    }

    @Override
    public boolean isLanguageDirectlySupportedByThisApplication(String language) {

        String applicationLanguages = ninjaProperties.get(NinjaConstant.applicationLanguages);
        Iterable<String> languages = Splitter.on(",").trimResults().split(applicationLanguages);

        for (String applicationLanguage : languages) {
            if (applicationLanguage.equals(language)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public Locale getLocaleFromStringOrDefault(Optional<String> language) {

        if (language.isPresent()) {

            return Locale.forLanguageTag(language.get());

        } else {

            return Locale.forLanguageTag(DEFAULT_LANGUAGE);

        }

    }

    String getDefaultLanguage(NinjaProperties ninjaProperties) {

        String [] applicationLanguages
                = ninjaProperties.getStringArray(NinjaConstant.applicationLanguages);

        if (applicationLanguages == null || applicationLanguages.length == 0) {

            String EXCEPTION_TEXT =
                    "Can not retrieve application languages from ninjaProperties."
                    + " Did you forget to define at least one language in your application.conf file?"
                    + " For instance 'application.languages=en' makes 'en' your default language.";

            throw new IllegalStateException(EXCEPTION_TEXT);

        }

        // by convention the first language is the default language
        String defaultLanguage = applicationLanguages[0];

        return defaultLanguage;

    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.utils;

import java.security.SecureRandom;
import java.util.Random;

public class SecretGenerator {

    /**
     * Generates a random String of length 64. This string is suitable
     * as secret for your application (key "application.secret" in conf/application.conf).
     *
     * @return A string that can be used as "application.secret".
     */
    public static String generateSecret() {

        return generateSecret(new SecureRandom());

    }

    /**
     * !!!! Only for testing purposes !!!!
     *
     * Usually you want to use {@link SecretGenerator#generateSecret()}
     *
     * @param random the random generator to use. Usually new Random(), but for testing you can
     *          use a predefined seed.
     * @return A String suitable as random secret for eg signing a session.
     */
    protected static String generateSecret(Random random) {

        String charsetForSecret = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

        StringBuilder stringBuilder = new StringBuilder(64);

        for (int i = 0; i < 64; i++) {

            int charToPoPickFromCharset = random.nextInt(charsetForSecret.length());
            stringBuilder.append(charsetForSecret.charAt(charToPoPickFromCharset));

        }

        return stringBuilder.toString();

    }

}