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 -> isoEncode(entry.getKey()) + "=" + isoEncode(value)) 130 .collect(Collectors.joining("&"))) 131 .collect(Collectors.joining("&")); 132 // When constructing the new URI, we cannot pass the newQuery 133 // to the constructor because it would be encoded once more. 134 // So we build the most basic parseable URI with the newQuery 135 // and resolve it against the remaining information. 136 try { 137 return new URI(uri.getScheme(), uri.getAuthority(), null, null, 138 null).resolve( 139 uri.getRawPath() + "?" + newQuery 140 + (uri.getFragment() == null ? "" 141 : ("#" + uri.getRawFragment()))); 142 } catch (URISyntaxException e) { 143 throw new IllegalArgumentException(e); 144 } 145 } 146 147 /** 148 * Returns `URLEncoder.encode(value, "ISO-8859-1")`. 149 * 150 * @param value the value 151 * @return the result 152 */ 153 public static String isoEncode(String value) { 154 try { 155 return URLEncoder.encode(value, "ISO-8859-1"); 156 } catch (UnsupportedEncodingException e) { 157 // Cannot happen, ISO-8859-1 is specified to be supported 158 throw new IllegalStateException(e); 159 } 160 } 161}