001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2017-2018 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.webconsole.base;
020
021import java.io.UnsupportedEncodingException;
022import java.net.URI;
023import java.net.URISyntaxException;
024import java.net.URLEncoder;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Optional;
031import java.util.stream.Collectors;
032import javax.security.auth.Subject;
033import org.jgrapes.http.Session;
034
035/**
036 * 
037 */
038public final class WebConsoleUtils {
039
040    private WebConsoleUtils() {
041    }
042
043    /**
044     * Convenience method for retrieving the user from
045     * a {@link Subject} associated with the session.
046     * 
047     * @return the user principal
048     */
049    public static Optional<UserPrincipal> userFromSession(Session session) {
050        return Optional.ofNullable((Subject) session.get(Subject.class))
051            .flatMap(subject -> subject.getPrincipals(UserPrincipal.class)
052                .stream().findFirst());
053    }
054
055    /**
056     * Create a {@link URI} from a path. This is similar to calling
057     * `new URI(null, null, path, null)` with the {@link URISyntaxException}
058     * converted to a {@link IllegalArgumentException}.
059     * 
060     * @param path the path
061     * @return the uri
062     * @throws IllegalArgumentException if the string violates 
063     * RFC 2396
064     */
065    public static URI uriFromPath(String path) throws IllegalArgumentException {
066        try {
067            return new URI(null, null, path, null);
068        } catch (URISyntaxException e) {
069            throw new IllegalArgumentException(e);
070        }
071    }
072
073    /**
074     * Returns the query part of a URI as map. Note that query parts
075     * can have multiple entries with the same key.
076     *
077     * @param uri the uri
078     * @return the map
079     */
080    public static Map<String, List<String>> queryAsMap(URI uri) {
081        if (uri.getQuery() == null) {
082            return new HashMap<>();
083        }
084        @SuppressWarnings("PMD.UseConcurrentHashMap")
085        Map<String, List<String>> result = new HashMap<>();
086        Arrays.stream(uri.getQuery().split("&")).forEach(item -> {
087            String[] pair = item.split("=");
088            result.computeIfAbsent(pair[0], key -> new ArrayList<>())
089                .add(pair[1]);
090        });
091        return result;
092    }
093
094    /**
095     * Merge query parameters into an existing URI.
096     *
097     * @param uri the URI
098     * @param parameters the parameters
099     * @return the new URI
100     */
101    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
102    public static URI mergeQuery(URI uri, Map<String, String> parameters) {
103        Map<String, List<String>> oldQuery = queryAsMap(uri);
104        for (Map.Entry<String, String> entry : parameters.entrySet()) {
105            oldQuery.computeIfAbsent(entry.getKey(), key -> new ArrayList<>())
106                .add(entry.getValue());
107        }
108        String newQuery = oldQuery.entrySet().stream()
109            .map(entry -> entry.getValue().stream()
110                .map(
111                    value -> WebConsoleUtils.isoEncode(entry.getKey()) + "="
112                        + WebConsoleUtils.isoEncode(value))
113                .collect(Collectors.joining("&")))
114            .collect(Collectors.joining("&"));
115        // When constructing the new URI, we cannot pass the newQuery
116        // to the constructor because it would be encoded once more.
117        // So we build the most basic parseable URI with the newQuery
118        // and resolve it against the remaining information.
119        try {
120            return new URI(uri.getScheme(), uri.getAuthority(), null, null,
121                null).resolve(
122                    uri.getRawPath() + "?" + newQuery
123                        + (uri.getFragment() == null ? ""
124                            : ("#" + uri.getRawFragment())));
125        } catch (URISyntaxException e) {
126            throw new IllegalArgumentException(e);
127        }
128    }
129
130    /**
131     * Returns `URLEncoder.encode(value, "ISO-8859-1")`.
132     *
133     * @param value the value
134     * @return the result
135     */
136    public static String isoEncode(String value) {
137        try {
138            return URLEncoder.encode(value, "ISO-8859-1");
139        } catch (UnsupportedEncodingException e) {
140            // Cannot happen, ISO-8859-1 is specified to be supported
141            throw new IllegalStateException(e);
142        }
143    }
144}