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