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.Collection;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Optional;
033import java.util.stream.Collectors;
034import javax.security.auth.Subject;
035import org.jgrapes.http.Session;
036
037/**
038 * 
039 */
040public final class WebConsoleUtils {
041
042    private WebConsoleUtils() {
043    }
044
045    /**
046     * Convenience method for retrieving the console user from
047     * a {@link Subject} associated with the session.
048     *
049     * @param session the session
050     * @return the user principal
051     */
052    public static Optional<ConsoleUser> userFromSession(Session session) {
053        return Optional.ofNullable((Subject) session.get(Subject.class))
054            .flatMap(subject -> subject.getPrincipals(ConsoleUser.class)
055                .stream().findFirst());
056    }
057
058    /**
059     * Convenience method for retrieving the console roles from
060     * a {@link Subject} associated with the session.
061     *
062     * @param session the session
063     * @return the role principals
064     */
065    public static Collection<ConsoleRole> rolesFromSession(Session session) {
066        return Optional.ofNullable((Subject) session.get(Subject.class))
067            .map(subject -> subject.getPrincipals(ConsoleRole.class))
068            .orElse(Collections.emptySet());
069    }
070
071    /**
072     * Create a {@link URI} from a path. This is similar to calling
073     * `new URI(null, null, path, null)` with the {@link URISyntaxException}
074     * converted to a {@link IllegalArgumentException}.
075     * 
076     * @param path the path
077     * @return the uri
078     * @throws IllegalArgumentException if the string violates 
079     * RFC 2396
080     */
081    @SuppressWarnings("PMD.AvoidUncheckedExceptionsInSignatures")
082    public static URI uriFromPath(String path) throws IllegalArgumentException {
083        try {
084            return new URI(null, null, path, null);
085        } catch (URISyntaxException e) {
086            throw new IllegalArgumentException(e);
087        }
088    }
089
090    /**
091     * Returns the query part of a URI as map. Note that query parts
092     * can have multiple entries with the same key.
093     *
094     * @param uri the uri
095     * @return the map
096     */
097    public static Map<String, List<String>> queryAsMap(URI uri) {
098        if (uri.getQuery() == null) {
099            return new HashMap<>();
100        }
101        @SuppressWarnings("PMD.UseConcurrentHashMap")
102        Map<String, List<String>> result = new HashMap<>();
103        Arrays.stream(uri.getQuery().split("&")).forEach(item -> {
104            String[] pair = item.split("=");
105            result.computeIfAbsent(pair[0], key -> new ArrayList<>())
106                .add(pair[1]);
107        });
108        return result;
109    }
110
111    /**
112     * Merge query parameters into an existing URI.
113     *
114     * @param uri the URI
115     * @param parameters the parameters
116     * @return the new URI
117     */
118    @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
119        "PMD.UselessParentheses" })
120    public static URI mergeQuery(URI uri, Map<String, String> parameters) {
121        Map<String, List<String>> oldQuery = queryAsMap(uri);
122        for (Map.Entry<String, String> entry : parameters.entrySet()) {
123            oldQuery.computeIfAbsent(entry.getKey(), key -> new ArrayList<>())
124                .add(entry.getValue());
125        }
126        String newQuery = oldQuery.entrySet().stream()
127            .map(entry -> entry.getValue().stream()
128                .map(
129                    value -> WebConsoleUtils.isoEncode(entry.getKey()) + "="
130                        + WebConsoleUtils.isoEncode(value))
131                .collect(Collectors.joining("&")))
132            .collect(Collectors.joining("&"));
133        // When constructing the new URI, we cannot pass the newQuery
134        // to the constructor because it would be encoded once more.
135        // So we build the most basic parseable URI with the newQuery
136        // and resolve it against the remaining information.
137        try {
138            return new URI(uri.getScheme(), uri.getAuthority(), null, null,
139                null).resolve(
140                    uri.getRawPath() + "?" + newQuery
141                        + (uri.getFragment() == null ? ""
142                            : ("#" + uri.getRawFragment())));
143        } catch (URISyntaxException e) {
144            throw new IllegalArgumentException(e);
145        }
146    }
147
148    /**
149     * Returns `URLEncoder.encode(value, "ISO-8859-1")`.
150     *
151     * @param value the value
152     * @return the result
153     */
154    public static String isoEncode(String value) {
155        try {
156            return URLEncoder.encode(value, "ISO-8859-1");
157        } catch (UnsupportedEncodingException e) {
158            // Cannot happen, ISO-8859-1 is specified to be supported
159            throw new IllegalStateException(e);
160        }
161    }
162}