diff --git a/README.md b/README.md index c9e5116..4098c52 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,12 @@ public class Application { public static void main(String... args) { ComponentProvider myComponentProvider = new MyComponentProvider(...); - Collection> entryPoints = ... + Collection> resources = ... RoutingConfig config = RoutingConfig.builder(myComponentProvider) - .addResource(MyEntryPoint.class) - .addResource(entryPoints) + .addResource(MyResource.class) + .addResource(resources) .addBodyWriter(new SimpleGsonBodyWriter<>()) .addBodyReader(new SimpleGsonBodyReader<>()) .addBodyWriter(new SimpleThymeleafBodyWriter("/thymeleaf/")) @@ -138,6 +138,7 @@ The type of the annotated parameter must either: * javax.servlet.http.HttpServletResponse * javax.servlet.ServletContext * javax.ws.rs.core.SecurityContext + * javax.ws.rs.core.HttpHeaders; 1. `javax.ws.rs.core.Response` ### Extensions diff --git a/core/src/main/java/net/cactusthorn/routing/RoutingServlet.java b/core/src/main/java/net/cactusthorn/routing/RoutingServlet.java index 2234845..81c68b1 100644 --- a/core/src/main/java/net/cactusthorn/routing/RoutingServlet.java +++ b/core/src/main/java/net/cactusthorn/routing/RoutingServlet.java @@ -25,6 +25,7 @@ import net.cactusthorn.routing.invoke.MethodInvoker.ReturnObjectInfo; import net.cactusthorn.routing.resource.ResourceScanner; import net.cactusthorn.routing.resource.ResourceScanner.Resource; +import net.cactusthorn.routing.util.Headers; import net.cactusthorn.routing.util.Http; import net.cactusthorn.routing.PathTemplate.PathValues; @@ -112,7 +113,7 @@ private void process(HttpServletRequest req, HttpServletResponse resp, List accept = Http.parseAccept(req.getHeaders(HttpHeaders.ACCEPT)); + List accept = Headers.parseAccept(req.getHeader(HttpHeaders.ACCEPT)); boolean matchContentTypeFail = false; boolean matchAcceptFail = false; for (Resource resource : resources) { diff --git a/core/src/main/java/net/cactusthorn/routing/delegate/LanguageHeaderDelegate.java b/core/src/main/java/net/cactusthorn/routing/delegate/LanguageHeaderDelegate.java new file mode 100644 index 0000000..82a7333 --- /dev/null +++ b/core/src/main/java/net/cactusthorn/routing/delegate/LanguageHeaderDelegate.java @@ -0,0 +1,43 @@ +package net.cactusthorn.routing.delegate; + +import java.util.Locale; + +import javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate; + +import net.cactusthorn.routing.util.Headers; +import net.cactusthorn.routing.util.Language; + +public class LanguageHeaderDelegate implements HeaderDelegate { + + @Override // + public Language fromString(String languageTag) { + if (languageTag == null) { + throw new IllegalArgumentException("languageTag can not be null"); + } + String[] parts = languageTag.split(";"); + String localeStr = parts[0].trim(); + Locale locale = null; + if ("*".equals(localeStr)) { + locale = Locale.forLanguageTag(localeStr); + } else { + locale = new Locale.Builder().setLanguageTag(localeStr).build(); + } + String q = null; + if (parts.length > 1) { + String[] subParts = Headers.getSubParts(parts[1]); + if ("q".equals(subParts[0])) { + q = subParts[1]; + } + } + return new Language(locale, q); + } + + @Override // + public String toString(Language language) { + if (language == null) { + throw new IllegalArgumentException("language can not be null"); + } + return language.getLocale().toLanguageTag() + ";q=" + language.getQ(); + } + +} diff --git a/core/src/main/java/net/cactusthorn/routing/delegate/RuntimeDelegateImpl.java b/core/src/main/java/net/cactusthorn/routing/delegate/RuntimeDelegateImpl.java index fe8ac5e..58577f5 100644 --- a/core/src/main/java/net/cactusthorn/routing/delegate/RuntimeDelegateImpl.java +++ b/core/src/main/java/net/cactusthorn/routing/delegate/RuntimeDelegateImpl.java @@ -18,6 +18,8 @@ import javax.ws.rs.core.Variant.VariantListBuilder; import javax.ws.rs.ext.RuntimeDelegate; +import net.cactusthorn.routing.util.Language; + public class RuntimeDelegateImpl extends RuntimeDelegate { private final Map, HeaderDelegate> headerDelegates = new HashMap<>(); @@ -32,6 +34,7 @@ public RuntimeDelegateImpl() { headerDelegates.put(NewCookie.class, new NewCookieHeaderDelegate()); headerDelegates.put(CacheControl.class, new CacheControlHeaderDelegate()); headerDelegates.put(Cookie.class, new CookieHeaderDelegate()); + headerDelegates.put(Language.class, new LanguageHeaderDelegate()); } @Override // diff --git a/core/src/main/java/net/cactusthorn/routing/invoke/HttpHeadersParameter.java b/core/src/main/java/net/cactusthorn/routing/invoke/HttpHeadersParameter.java new file mode 100644 index 0000000..e181efd --- /dev/null +++ b/core/src/main/java/net/cactusthorn/routing/invoke/HttpHeadersParameter.java @@ -0,0 +1,164 @@ +package net.cactusthorn.routing.invoke; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.StringJoiner; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.RuntimeDelegate; + +import net.cactusthorn.routing.PathTemplate.PathValues; +import net.cactusthorn.routing.util.CaseInsensitiveMultivaluedMap; +import net.cactusthorn.routing.util.Headers; +import net.cactusthorn.routing.util.UnmodifiableMultivaluedMap; + +public class HttpHeadersParameter extends MethodParameter { + + public HttpHeadersParameter(Method method, Parameter parameter, Type genericType, int position) { + super(method, parameter, genericType, position); + } + + @Override // + public HttpHeaders findValue(HttpServletRequest req, HttpServletResponse res, ServletContext con, PathValues pathValues) + throws Exception { + return new HttpHeadersImpl(req); + } + + static final class HttpHeadersImpl implements HttpHeaders { + + private static final MultivaluedMap EMPTY = new UnmodifiableMultivaluedMap<>(new MultivaluedHashMap<>()); + + private MultivaluedMap map; + + private Locale locale; + private MediaType mediaType; + private Date date; + private List accept; + private Map cookies; + private List acceptLanguage; + + HttpHeadersImpl(HttpServletRequest req) { + Enumeration headerNames = req.getHeaderNames(); + if (headerNames == null || !headerNames.hasMoreElements()) { + map = EMPTY; + } else { + MultivaluedMap headers = new CaseInsensitiveMultivaluedMap(); + for (Enumeration names = headerNames; names.hasMoreElements();) { + String name = names.nextElement(); + headers.addAll(name, Collections.list(req.getHeaders(name))); + } + map = new UnmodifiableMultivaluedMap<>(headers); + } + locale = parseLocale(); + mediaType = parseMediaType(); + date = parseDate(); + accept = Headers.parseAccept(map.getFirst(ACCEPT)); + cookies = parseCookies(); + acceptLanguage = Headers.parseAcceptLanguage(map.getFirst(ACCEPT_LANGUAGE)); + } + + @Override public List getRequestHeader(String name) { + return map.get(name); + } + + @Override public String getHeaderString(String name) { + List values = map.get(name); + if (values == null) { + return null; + } + StringJoiner joiner = new StringJoiner(","); + for (String value : values) { + joiner.add(value); + } + return joiner.toString(); + } + + @Override public MultivaluedMap getRequestHeaders() { + return map; + } + + @Override public List getAcceptableMediaTypes() { + return accept; + } + + @Override public List getAcceptableLanguages() { + return acceptLanguage; + } + + @Override public MediaType getMediaType() { + return mediaType; + } + + @Override public Locale getLanguage() { + return locale; + } + + @Override public Map getCookies() { + return cookies; + } + + @Override public Date getDate() { + return date; + } + + @Override public int getLength() { + String header = map.getFirst(CONTENT_LENGTH); + if (header == null) { + return -1; + } + return Integer.parseInt(header); + } + + private Locale parseLocale() { + String header = map.getFirst(CONTENT_LANGUAGE); + if (header == null) { + return null; + } + return RuntimeDelegate.getInstance().createHeaderDelegate(Locale.class).fromString(header); + } + + private MediaType parseMediaType() { + String header = map.getFirst(CONTENT_TYPE); + if (header == null) { + return null; + } + return MediaType.valueOf(header); + } + + private Date parseDate() { + String header = map.getFirst(DATE); + if (header == null) { + return null; + } + return RuntimeDelegate.getInstance().createHeaderDelegate(Date.class).fromString(header); + } + + private Map parseCookies() { + String header = map.getFirst(COOKIE); + if (header == null) { + return Collections.emptyMap(); + } + Map result = new HashMap<>(); + List list = Headers.parseCookies(header); + for (Cookie cookie : list) { + result.putIfAbsent(cookie.getName(), cookie); + } + return Collections.unmodifiableMap(result); + } + } +} diff --git a/core/src/main/java/net/cactusthorn/routing/invoke/MethodParameterFactory.java b/core/src/main/java/net/cactusthorn/routing/invoke/MethodParameterFactory.java index 5897e32..4749c87 100644 --- a/core/src/main/java/net/cactusthorn/routing/invoke/MethodParameterFactory.java +++ b/core/src/main/java/net/cactusthorn/routing/invoke/MethodParameterFactory.java @@ -21,6 +21,7 @@ import javax.ws.rs.PUT; import javax.ws.rs.PATCH; import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; import net.cactusthorn.routing.RoutingConfig; import net.cactusthorn.routing.RoutingInitializationException; @@ -75,6 +76,9 @@ static MethodParameter create(Method method, Parameter parameter, Type genericTy if (SecurityContext.class == parameter.getType()) { return new SecurityContextParameter(method, parameter, genericType, position); } + if (HttpHeaders.class == parameter.getType()) { + return new HttpHeadersParameter(method, parameter, genericType, position); + } throw new RoutingInitializationException(CONTEXT_NOT_SUPPORTED, parameter.getType(), method); } if (method.getAnnotation(POST.class) != null || method.getAnnotation(PUT.class) != null diff --git a/core/src/main/java/net/cactusthorn/routing/resource/ProducesParser.java b/core/src/main/java/net/cactusthorn/routing/resource/ProducesParser.java index 8ab4933..dfd5c8e 100644 --- a/core/src/main/java/net/cactusthorn/routing/resource/ProducesParser.java +++ b/core/src/main/java/net/cactusthorn/routing/resource/ProducesParser.java @@ -8,7 +8,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import net.cactusthorn.routing.util.Http; +import net.cactusthorn.routing.util.Headers; public final class ProducesParser { @@ -38,7 +38,7 @@ private List parseProduces(String[] consumes) { mediaTypes.add(new MediaType(parts[0], parts[1])); } } - Collections.sort(mediaTypes, Http.ACCEPT_COMPARATOR); + Collections.sort(mediaTypes, Headers.ACCEPT_COMPARATOR); return mediaTypes; } } diff --git a/core/src/main/java/net/cactusthorn/routing/util/CaseInsensitiveMultivaluedMap.java b/core/src/main/java/net/cactusthorn/routing/util/CaseInsensitiveMultivaluedMap.java new file mode 100644 index 0000000..9f5b3ff --- /dev/null +++ b/core/src/main/java/net/cactusthorn/routing/util/CaseInsensitiveMultivaluedMap.java @@ -0,0 +1,133 @@ +package net.cactusthorn.routing.util; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; + +public class CaseInsensitiveMultivaluedMap implements MultivaluedMap { + + private final MultivaluedMap map; + + public CaseInsensitiveMultivaluedMap() { + map = new MultivaluedHashMap<>(); + } + + public CaseInsensitiveMultivaluedMap(int initialCapacity) { + map = new MultivaluedHashMap<>(initialCapacity); + } + + public CaseInsensitiveMultivaluedMap(int initialCapacity, float loadFactor) { + map = new MultivaluedHashMap<>(initialCapacity, loadFactor); + } + + // MultivaluedMap { + @Override public void putSingle(String key, V value) { + map.putSingle(key.toLowerCase(), value); + } + + @Override public void add(String key, V value) { + map.add(key.toLowerCase(), value); + } + + @Override public V getFirst(String key) { + return map.getFirst(key.toLowerCase()); + } + + @Override @SuppressWarnings("unchecked") public void addAll(String key, V... newValues) { + map.addAll(key.toLowerCase(), newValues); + } + + @Override public void addAll(String key, List valueList) { + map.addAll(key.toLowerCase(), valueList); + } + + @Override public void addFirst(String key, V value) { + map.addFirst(key.toLowerCase(), value); + } + + @Override public boolean equalsIgnoreValueOrder(MultivaluedMap otherMap) { + return map.equalsIgnoreValueOrder(otherMap); + } + // MultivaluedMap } + + // Map { + @Override public int size() { + return map.size(); + } + + @Override public boolean isEmpty() { + return map.isEmpty(); + } + + @Override public boolean containsKey(Object key) { + if (!String.class.isAssignableFrom(key.getClass())) { + return false; + } + return map.containsKey(((String) key).toLowerCase()); + } + + @Override public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override public List get(Object key) { + if (!String.class.isAssignableFrom(key.getClass())) { + return null; + } + return map.get(((String) key).toLowerCase()); + } + + @Override public List put(String key, List value) { + return map.put(key.toLowerCase(), value); + } + + @Override public List remove(Object key) { + if (!String.class.isAssignableFrom(key.getClass())) { + return null; + } + return map.remove(((String) key).toLowerCase()); + } + + @Override public void putAll(Map> m) { + for (Map.Entry> entry : m.entrySet()) { + map.put(entry.getKey().toLowerCase(), entry.getValue()); + } + } + + @Override public void clear() { + map.clear(); + + } + + @Override public Set keySet() { + Set result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + result.addAll(map.keySet()); + return result; + } + + @Override public Collection> values() { + return map.values(); + } + + @Override public Set>> entrySet() { + return map.entrySet(); + } + // Map } + + @Override public String toString() { + return map.toString(); + } + + @Override public int hashCode() { + return map.hashCode(); + } + + @Override public boolean equals(Object obj) { + return map.equals(obj); + } +} diff --git a/core/src/main/java/net/cactusthorn/routing/util/Headers.java b/core/src/main/java/net/cactusthorn/routing/util/Headers.java index 8a53494..f843464 100644 --- a/core/src/main/java/net/cactusthorn/routing/util/Headers.java +++ b/core/src/main/java/net/cactusthorn/routing/util/Headers.java @@ -1,12 +1,98 @@ package net.cactusthorn.routing.util; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.Locale; import java.util.stream.Collectors; import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.MediaType; -public class Headers { +public final class Headers { + + public static final class AcceptComparator implements Comparator { + + @Override // + public int compare(MediaType o1, MediaType o2) { + if (o1 == null && o2 == null) { + return 0; + } + if (o1 == null) { + return 1; + } + if (o2 == null) { + return -1; + } + + if (o1.isWildcardType() && !o2.isWildcardType()) { + return 1; + } + if (!o1.isWildcardType() && o2.isWildcardType()) { + return -1; + } + if (o1.isWildcardSubtype() && !o2.isWildcardSubtype()) { + return 1; + } + if (!o1.isWildcardSubtype() && o2.isWildcardSubtype()) { + return -1; + } + + double q1 = getQ(o1); + double q2 = getQ(o2); + if (q1 > q2) { + return -1; + } + if (q2 > q1) { + return 1; + } + return 0; + } + + private double getQ(MediaType mediaType) { + String q = mediaType.getParameters().get("q"); + if (q == null) { + return 1d; + } + return Double.parseDouble(q); + } + }; + + public static final class AcceptLanguageComparator implements Comparator { + + @Override // + public int compare(Language o1, Language o2) { + if (o1 == null && o2 == null) { + return 0; + } + if (o1 == null) { + return 1; + } + if (o2 == null) { + return -1; + } + + double q1 = getQ(o1); + double q2 = getQ(o2); + if (q1 > q2) { + return -1; + } + if (q2 > q1) { + return 1; + } + return 0; + } + + private double getQ(Language language) { + String q = language.getQ(); + return Double.parseDouble(q); + } + }; + + public static final Comparator ACCEPT_COMPARATOR = new AcceptComparator(); + + public static final Comparator ACCEPT_LANGUAGE_COMPARATOR = new AcceptLanguageComparator(); public static boolean containsWhiteSpace(String str) { for (char c : str.toCharArray()) { @@ -44,7 +130,7 @@ public static String[] getSubParts(String str) { return new String[] {str.substring(0, valueStart).trim(), value}; } - //RFC 2109 + // RFC 2109 public static List parseCookies(String cookieHeader) { List result = new ArrayList<>(); @@ -85,4 +171,37 @@ public static List parseCookies(String cookieName, String cookieHeader) List result = parseCookies(cookieHeader); return result.stream().filter(c -> c.getName().equals(cookieName)).collect(Collectors.toList()); } + + public static List parseAccept(String acceptHeader) { + List mediaTypes = new ArrayList<>(); + if (acceptHeader != null) { + String[] parts = acceptHeader.split(","); + for (String part : parts) { + mediaTypes.add(MediaType.valueOf(part)); + } + } + if (mediaTypes.isEmpty()) { + mediaTypes.add(MediaType.WILDCARD_TYPE); + } else { + Collections.sort(mediaTypes, ACCEPT_COMPARATOR); + } + return Collections.unmodifiableList(mediaTypes); + } + + public static List parseAcceptLanguage(String acceptLanguageHeader) { + if (acceptLanguageHeader == null || acceptLanguageHeader.trim().isEmpty()) { + return Collections.emptyList(); + } + List languages = new ArrayList<>(); + String[] parts = acceptLanguageHeader.split(","); + for (String part : parts) { + languages.add(Language.valueOf(part)); + } + Collections.sort(languages, ACCEPT_LANGUAGE_COMPARATOR); + List locales = new ArrayList<>(); + for (Language language : languages) { + locales.add(language.getLocale()); + } + return Collections.unmodifiableList(locales); + } } diff --git a/core/src/main/java/net/cactusthorn/routing/util/Http.java b/core/src/main/java/net/cactusthorn/routing/util/Http.java index a0c7a19..a24e4e3 100644 --- a/core/src/main/java/net/cactusthorn/routing/util/Http.java +++ b/core/src/main/java/net/cactusthorn/routing/util/Http.java @@ -1,62 +1,14 @@ package net.cactusthorn.routing.util; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Enumeration; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.RuntimeDelegate; import javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate; -public class Http { - - public static final Comparator ACCEPT_COMPARATOR = (o1, o2) -> { - if (o1 == null && o2 == null) { - return 0; - } - if (o1 == null) { - return 1; - } - if (o2 == null) { - return -1; - } - - if (o1.isWildcardType() && !o2.isWildcardType()) { - return 1; - } - if (!o1.isWildcardType() && o2.isWildcardType()) { - return -1; - } - if (o1.isWildcardSubtype() && !o2.isWildcardSubtype()) { - return 1; - } - if (!o1.isWildcardSubtype() && o2.isWildcardSubtype()) { - return -1; - } - - double q1 = getQ(o1); - double q2 = getQ(o2); - if (q1 > q2) { - return -1; - } - if (q2 > q1) { - return 1; - } - return 0; - }; - - private static double getQ(MediaType mediaType) { - String q = mediaType.getParameters().get("q"); - if (q == null) { - return 1d; - } - return Double.parseDouble(q); - } +public final class Http { @SuppressWarnings("unchecked") // public static void writeHeaders(HttpServletResponse response, MultivaluedMap headers) { @@ -79,21 +31,4 @@ public static void writeHeaders(HttpServletResponse response, MultivaluedMap parseAccept(Enumeration acceptHeader) { - List mediaTypes = new ArrayList<>(); - for (Enumeration e = acceptHeader; e.hasMoreElements();) { - String header = e.nextElement(); - String[] parts = header.split(","); - for (String part : parts) { - mediaTypes.add(MediaType.valueOf(part)); - } - } - if (mediaTypes.isEmpty()) { - mediaTypes.add(MediaType.WILDCARD_TYPE); - } else { - Collections.sort(mediaTypes, ACCEPT_COMPARATOR); - } - return mediaTypes; - } } diff --git a/core/src/main/java/net/cactusthorn/routing/util/Language.java b/core/src/main/java/net/cactusthorn/routing/util/Language.java new file mode 100644 index 0000000..0219fb6 --- /dev/null +++ b/core/src/main/java/net/cactusthorn/routing/util/Language.java @@ -0,0 +1,39 @@ +package net.cactusthorn.routing.util; + +import java.util.Locale; + +import javax.ws.rs.ext.RuntimeDelegate; + +public class Language { + + private Locale locale; + private String q; + + public Language(Locale locale) { + this.locale = locale; + } + + public Language(Locale locale, String q) { + this.locale = locale; + this.q = q; + } + + public Locale getLocale() { + return locale; + } + + public String getQ() { + if (q == null) { + return "1.0"; + } + return q; + } + + public static Language valueOf(String language) { + return RuntimeDelegate.getInstance().createHeaderDelegate(Language.class).fromString(language); + } + + @Override public String toString() { + return RuntimeDelegate.getInstance().createHeaderDelegate(Language.class).toString(this); + } +} diff --git a/core/src/main/java/net/cactusthorn/routing/util/UnmodifiableMultivaluedMap.java b/core/src/main/java/net/cactusthorn/routing/util/UnmodifiableMultivaluedMap.java index ccfe6dd..ffe08cc 100644 --- a/core/src/main/java/net/cactusthorn/routing/util/UnmodifiableMultivaluedMap.java +++ b/core/src/main/java/net/cactusthorn/routing/util/UnmodifiableMultivaluedMap.java @@ -127,4 +127,16 @@ private UnmodifiableEntry(Map.Entry> entry) { return entrySet; } // Map } + + @Override public String toString() { + return map.toString(); + } + + @Override public int hashCode() { + return map.hashCode(); + } + + @Override public boolean equals(Object obj) { + return map.equals(obj); + } } diff --git a/core/src/test/java/net/cactusthorn/routing/RoutingServletTest.java b/core/src/test/java/net/cactusthorn/routing/RoutingServletTest.java index 7200788..73a2742 100644 --- a/core/src/test/java/net/cactusthorn/routing/RoutingServletTest.java +++ b/core/src/test/java/net/cactusthorn/routing/RoutingServletTest.java @@ -294,9 +294,7 @@ public void post() throws ServletException, IOException { public void notAccept() throws ServletException, IOException { Mockito.when(req.getPathInfo()).thenReturn("/api/post"); Mockito.when(req.getMethod()).thenReturn(HttpMethod.POST); - List accept = new ArrayList<>(); - accept.add("application/json"); - Mockito.when(req.getHeaders(HttpHeaders.ACCEPT)).thenReturn(Collections.enumeration(accept)); + Mockito.when(req.getHeader(HttpHeaders.ACCEPT)).thenReturn(MediaType.APPLICATION_JSON); servlet.doPost(req, resp); @@ -498,9 +496,7 @@ public void userRoles() throws ServletException, IOException { public void wrongProduces() throws ServletException, IOException { Mockito.when(req.getPathInfo()).thenReturn("/api/wrong/produces"); Mockito.when(req.getMethod()).thenReturn(HttpMethod.GET); - List accept = new ArrayList<>(); - accept.add(MediaType.TEXT_HTML); - Mockito.when(req.getHeaders(HttpHeaders.ACCEPT)).thenReturn(Collections.enumeration(accept)); + Mockito.when(req.getHeader(HttpHeaders.ACCEPT)).thenReturn(MediaType.TEXT_HTML); servlet.doGet(req, resp); diff --git a/core/src/test/java/net/cactusthorn/routing/delegate/LanguageHeaderDelegateTest.java b/core/src/test/java/net/cactusthorn/routing/delegate/LanguageHeaderDelegateTest.java new file mode 100644 index 0000000..3597f7c --- /dev/null +++ b/core/src/test/java/net/cactusthorn/routing/delegate/LanguageHeaderDelegateTest.java @@ -0,0 +1,20 @@ +package net.cactusthorn.routing.delegate; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class LanguageHeaderDelegateTest { + + private static final LanguageHeaderDelegate DELEGATE = new LanguageHeaderDelegate(); + + @Test + public void fromNull() { + assertThrows(IllegalArgumentException.class, () -> DELEGATE.fromString(null)); + } + + @Test + public void toNull() { + assertThrows(IllegalArgumentException.class, () -> DELEGATE.toString(null)); + } +} diff --git a/core/src/test/java/net/cactusthorn/routing/invoke/HttpHeadersImplTest.java b/core/src/test/java/net/cactusthorn/routing/invoke/HttpHeadersImplTest.java new file mode 100644 index 0000000..78e0bf1 --- /dev/null +++ b/core/src/test/java/net/cactusthorn/routing/invoke/HttpHeadersImplTest.java @@ -0,0 +1,106 @@ +package net.cactusthorn.routing.invoke; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class HttpHeadersImplTest extends InvokeTestAncestor { + + @Test public void getRequestHeader() { + HttpHeaders headers = httpHeaders("test-header", "test-value"); + assertEquals("test-value", headers.getRequestHeader("test-header").get(0)); + } + + @Test public void getRequestHeaders() { + HttpHeaders headers = httpHeaders("test-header", "test-value"); + MultivaluedMap result = headers.getRequestHeaders(); + assertEquals("test-value", result.getFirst("test-header")); + } + + @Test public void getHeaderString() { + HttpHeaders headers = httpHeaders("test-header", "test-value"); + assertEquals("test-value", headers.getHeaderString("test-header")); + assertNull(headers.getHeaderString("test-header2")); + } + + @Test public void nullHeaders() { + Mockito.when(request.getHeaderNames()).thenReturn(null); + HttpHeaders headers = new HttpHeadersParameter.HttpHeadersImpl(request); + assertNotNull(headers); + } + + @Test public void emptyHeaders() { + Mockito.when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration()); + HttpHeaders headers = new HttpHeadersParameter.HttpHeadersImpl(request); + assertNotNull(headers); + } + + @Test public void getAcceptableMediaTypes() { + HttpHeaders headers = httpHeaders(HttpHeaders.ACCEPT, + "image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"); + List types = headers.getAcceptableMediaTypes(); + assertEquals(5, types.size()); + assertEquals(MediaType.valueOf("application/signed-exchange;v=b3;q=0.9"), types.get(3)); + } + + @Test public void getAcceptableLanguages() { + HttpHeaders headers = httpHeaders(HttpHeaders.ACCEPT_LANGUAGE, "fr-CH, en;q=0.8, de;q=0.7, *;q=0.5, fr;q=0.9"); + List locales = headers.getAcceptableLanguages(); + assertEquals(5, locales.size()); + assertEquals(new Locale("fr"), locales.get(1)); + } + + @Test public void getMediaType() { + HttpHeaders headers = httpHeaders(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + MediaType type = headers.getMediaType(); + assertEquals(MediaType.APPLICATION_JSON_TYPE, type); + } + + @Test public void getCookies() { + HttpHeaders headers = httpHeaders(HttpHeaders.COOKIE, "aa=bbb, cc=ddd"); + Map cookies = headers.getCookies(); + assertEquals("bbb", cookies.get("aa").getValue()); + assertEquals("ddd", cookies.get("cc").getValue()); + } + + @Test public void getDate() { + HttpHeaders headers = httpHeaders(HttpHeaders.DATE, "Thu, 01 Dec 1994 16:00:00 GMT"); + assertNotNull(headers.getDate()); + } + + @Test public void getLanguage() { + HttpHeaders headers = httpHeaders(HttpHeaders.CONTENT_LANGUAGE, "fr-CH"); + assertEquals(new Locale("fr", "CH"), headers.getLanguage()); + } + + @Test public void getNullLength() { + HttpHeaders headers = httpHeaders(HttpHeaders.CONTENT_LANGUAGE, "fr-CH"); + assertEquals(-1, headers.getLength()); + } + + @Test public void getLength() { + HttpHeaders headers = httpHeaders(HttpHeaders.CONTENT_LENGTH, "100"); + assertEquals(100, headers.getLength()); + } + + private HttpHeaders httpHeaders(String name, String value) { + Map map = new HashMap<>(); + map.put(name, value); + Mockito.when(request.getHeaderNames()).thenReturn(Collections.enumeration(map.keySet())); + Mockito.when(request.getHeaders(name)).thenReturn(Collections.enumeration(map.values())); + + return new HttpHeadersParameter.HttpHeadersImpl(request); + } +} diff --git a/core/src/test/java/net/cactusthorn/routing/invoke/HttpHeadersParameterTest.java b/core/src/test/java/net/cactusthorn/routing/invoke/HttpHeadersParameterTest.java new file mode 100644 index 0000000..7e7f99e --- /dev/null +++ b/core/src/test/java/net/cactusthorn/routing/invoke/HttpHeadersParameterTest.java @@ -0,0 +1,45 @@ +package net.cactusthorn.routing.invoke; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import net.cactusthorn.routing.ComponentProvider; +import net.cactusthorn.routing.RoutingConfig; + +public class HttpHeadersParameterTest extends InvokeTestAncestor { + + public static class EntryPoint1 { + public void simple(@Context HttpHeaders headers) { + } + } + + public static class EntryPoint1Provider implements ComponentProvider { + @Override public Object provide(Class clazz, HttpServletRequest request) { + return new EntryPoint1(); + } + } + + private static final RoutingConfig CONFIG = RoutingConfig.builder(new EntryPoint1Provider()).addResource(EntryPoint1.class).build(); + + @Test + public void findValue() throws Exception { + Map map = new HashMap<>(); + map.put("test-header", "test-value"); + Mockito.when(request.getHeaderNames()).thenReturn(Collections.enumeration(map.keySet())); + Mockito.when(request.getHeaders("test-header")).thenReturn(Collections.enumeration(map.values())); + + MethodParameter mp = parameterInfo(EntryPoint1.class, "simple", CONFIG); + HttpHeaders result = (HttpHeaders) mp.findValue(request, null, null, null); + assertEquals("test-value", result.getHeaderString("test-header")); + } +} diff --git a/core/src/test/java/net/cactusthorn/routing/resource/ScannerTest.java b/core/src/test/java/net/cactusthorn/routing/resource/ScannerTest.java index e4fb99a..b3bc149 100644 --- a/core/src/test/java/net/cactusthorn/routing/resource/ScannerTest.java +++ b/core/src/test/java/net/cactusthorn/routing/resource/ScannerTest.java @@ -3,8 +3,6 @@ import static org.junit.jupiter.api.Assertions.*; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -18,7 +16,7 @@ import net.cactusthorn.routing.RoutingConfig; import net.cactusthorn.routing.annotation.Template; import net.cactusthorn.routing.resource.ResourceScanner.Resource; -import net.cactusthorn.routing.util.Http; +import net.cactusthorn.routing.util.Headers; import javax.ws.rs.GET; import javax.ws.rs.HEAD; @@ -139,15 +137,12 @@ public void entryPoint3() { assertTrue(entryPoint.match("/")); - List header = new ArrayList<>(); - header.add(MediaType.APPLICATION_JSON); - header.add(MediaType.TEXT_PLAIN); - List accept = Http.parseAccept(Collections.enumeration(header)); + String header = MediaType.APPLICATION_JSON + ", " + MediaType.TEXT_PLAIN; + List accept = Headers.parseAccept(header); assertTrue(entryPoint.matchAccept(accept).isPresent()); - header.clear(); - header.add(MediaType.APPLICATION_JSON); - accept = Http.parseAccept(Collections.enumeration(header)); + header = MediaType.APPLICATION_JSON; + accept = Headers.parseAccept(header); assertFalse(entryPoint.matchAccept(accept).isPresent()); } @@ -182,9 +177,8 @@ public void entryPoint4() { assertEquals(PathValues.EMPTY, values); assertTrue(entryPoint.match("/api/")); - List header = new ArrayList<>(); - header.add(MediaType.WILDCARD); - List accept = Http.parseAccept(Collections.enumeration(header)); + String header = MediaType.WILDCARD; + List accept = Headers.parseAccept(header); assertTrue(entryPoint.matchAccept(accept).isPresent()); } diff --git a/core/src/test/java/net/cactusthorn/routing/util/CaseInsensitiveMultivaluedMapTest.java b/core/src/test/java/net/cactusthorn/routing/util/CaseInsensitiveMultivaluedMapTest.java new file mode 100644 index 0000000..5064816 --- /dev/null +++ b/core/src/test/java/net/cactusthorn/routing/util/CaseInsensitiveMultivaluedMapTest.java @@ -0,0 +1,85 @@ +package net.cactusthorn.routing.util; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.MultivaluedMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class CaseInsensitiveMultivaluedMapTest { + + static final List LIST = Arrays.asList("L1", "L2"); + static final List LIST_B = Arrays.asList("B"); + + Map> simpleMap; + + MultivaluedMap map; + + @BeforeEach // + public void setUp() { + map = new CaseInsensitiveMultivaluedMap<>(); + map.add("AA", "b"); + map.addFirst("AA", "c"); + map.add("AA", "f"); + map.addAll("BBB", LIST_B); + + simpleMap = new HashMap<>(); + simpleMap.put("ZZ", Arrays.asList("Z1")); + simpleMap.put("zz", Arrays.asList("Z2")); + } + + @Test // + public void multivaluedMap() { + assertEquals("c", map.getFirst("aA")); + map.putSingle("aa", "z"); + assertEquals("z", map.getFirst("aA")); + assertEquals(1, map.get("Aa").size()); + map.add("AA", "f"); + assertEquals(2, map.get("Aa").size()); + map.addAll("aa", "f1", "f2"); + assertEquals(4, map.get("Aa").size()); + map.addAll("aA", LIST); + assertEquals(6, map.get("Aa").size()); + assertEquals(2, map.size()); + assertTrue(map.equalsIgnoreValueOrder(map)); + } + + @Test @SuppressWarnings("unlikely-arg-type") // + public void map() { + assertFalse(map.isEmpty()); + assertFalse(map.containsKey(10)); + assertTrue(map.containsValue(LIST_B)); + assertNull(map.get(10)); + assertNotNull(map.remove("bBb")); + assertNull(map.remove(10)); + map.put("bbB", LIST_B); + assertTrue(map.containsKey("BBB")); + map.putAll(simpleMap); + assertEquals(1, map.get("zZ").size()); + assertNotNull(map.keySet()); + assertNotNull(map.values()); + assertNotNull(map.entrySet()); + map.clear(); + assertTrue(map.isEmpty()); + } + + @Test + public void object() { + assertEquals("{aa=[c, b, f], bbb=[B]}", map.toString()); + assertEquals(map, map); + assertTrue(map.hashCode() > 0); + } + + @Test // + public void constructor() { + assertNotNull(new CaseInsensitiveMultivaluedMap(10)); + assertNotNull(new CaseInsensitiveMultivaluedMap(10, 0.75f)); + } + +} diff --git a/core/src/test/java/net/cactusthorn/routing/util/HeadersTest.java b/core/src/test/java/net/cactusthorn/routing/util/HeadersTest.java new file mode 100644 index 0000000..6577ddc --- /dev/null +++ b/core/src/test/java/net/cactusthorn/routing/util/HeadersTest.java @@ -0,0 +1,57 @@ +package net.cactusthorn.routing.util; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +public class HeadersTest { + + @Test // + public void languageSort() { + + List list = new ArrayList<>(); + list.add(new Language(Locale.GERMANY, "0.5")); + list.add(new Language(Locale.FRANCE)); + list.add(null); + list.add(null); + list.add(new Language(Locale.US)); + list.add(new Language(Locale.UK, "0.875")); + list.add(null); + + Collections.sort(list, Headers.ACCEPT_LANGUAGE_COMPARATOR); + + assertEquals(Locale.FRANCE, list.get(0).getLocale()); + assertEquals(Locale.US, list.get(1).getLocale()); + assertEquals(Locale.UK, list.get(2).getLocale()); + assertEquals(Locale.GERMANY, list.get(3).getLocale()); + assertNull(list.get(4)); + assertNull(list.get(5)); + assertNull(list.get(6)); + } + + @Test // + public void parseEmptyAcceptLanguage() { + List locales = Headers.parseAcceptLanguage(null); + assertTrue(locales.isEmpty()); + locales = Headers.parseAcceptLanguage(" "); + assertTrue(locales.isEmpty()); + locales = Headers.parseAcceptLanguage("*"); + assertEquals("", locales.get(0).toString()); + } + + @Test // + public void parseAcceptLanguage() { + List locales = Headers.parseAcceptLanguage("fr-CH, en;q=0.8, de;q=0.7, *;q=0.5, fr;q=0.9"); + assertEquals(5, locales.size()); + assertEquals(new Locale("fr","CH"), locales.get(0)); + assertEquals(new Locale("fr"), locales.get(1)); + assertEquals(new Locale("en"), locales.get(2)); + assertEquals(new Locale("de"), locales.get(3)); + assertEquals(Locale.forLanguageTag("*"), locales.get(4)); + } +} diff --git a/core/src/test/java/net/cactusthorn/routing/util/HttpTest.java b/core/src/test/java/net/cactusthorn/routing/util/HttpTest.java index b26c89a..b891426 100644 --- a/core/src/test/java/net/cactusthorn/routing/util/HttpTest.java +++ b/core/src/test/java/net/cactusthorn/routing/util/HttpTest.java @@ -29,7 +29,7 @@ public void sort() { list.add(new MediaType("*", "json")); list.add(null); - Collections.sort(list, Http.ACCEPT_COMPARATOR); + Collections.sort(list, Headers.ACCEPT_COMPARATOR); assertEquals("text/plain", _toString(list.get(0))); assertEquals("application/json", _toString(list.get(1))); @@ -57,11 +57,10 @@ private String _toString(MediaType mediaType) { @Test // public void parseAccept() { - List accept = new ArrayList<>(); - accept.add( - "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"); + String accept = "text/html,application/xhtml+xml,application/xml;" + + "q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"; - List mediaTypes = Http.parseAccept(Collections.enumeration(accept)); + List mediaTypes = Headers.parseAccept(accept); assertEquals("text/html", mediaTypes.get(0).toString()); assertEquals("application/xhtml+xml", mediaTypes.get(1).toString()); assertEquals("image/avif", mediaTypes.get(2).toString()); @@ -74,7 +73,7 @@ public void parseAccept() { @Test // public void parseEmptyAccept() { - List mediaTypes = Http.parseAccept(Collections.emptyEnumeration()); + List mediaTypes = Headers.parseAccept(null); assertEquals(MediaType.WILDCARD, mediaTypes.get(0).toString()); } diff --git a/core/src/test/java/net/cactusthorn/routing/util/LanguageTest.java b/core/src/test/java/net/cactusthorn/routing/util/LanguageTest.java new file mode 100644 index 0000000..1136d10 --- /dev/null +++ b/core/src/test/java/net/cactusthorn/routing/util/LanguageTest.java @@ -0,0 +1,29 @@ +package net.cactusthorn.routing.util; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +public class LanguageTest { + + @Test // + public void simple() { + Language language = new Language(Locale.GERMANY); + assertEquals("de-DE;q=1.0", language.toString()); + } + + @Test // + public void q() { + Language language = new Language(Locale.GERMAN, "0.3"); + assertEquals("de;q=0.3", language.toString()); + } + + @Test // + public void valueOf() { + Language language = Language.valueOf(" de-DE ; q=0.3 "); + assertEquals(Locale.GERMANY, language.getLocale()); + assertEquals("0.3", language.getQ()); + } +} diff --git a/core/src/test/java/net/cactusthorn/routing/util/UnmodifiableMultivaluedMapTest.java b/core/src/test/java/net/cactusthorn/routing/util/UnmodifiableMultivaluedMapTest.java index d6c7465..431632a 100644 --- a/core/src/test/java/net/cactusthorn/routing/util/UnmodifiableMultivaluedMapTest.java +++ b/core/src/test/java/net/cactusthorn/routing/util/UnmodifiableMultivaluedMapTest.java @@ -86,4 +86,11 @@ public void modifyEntrySet() { assertThrows(UnsupportedOperationException.class, () -> entries.add(null)); assertThrows(UnsupportedOperationException.class, () -> entry.setValue(LIST)); } + + @Test + public void object() { + assertEquals(MAP.toString(), U.toString()); + assertTrue(U.equals(MAP)); + assertEquals(MAP.hashCode(), U.hashCode()); + } } diff --git a/demo-jetty/src/main/java/net/cactusthorn/routing/demo/jetty/resource/SimpleResource.java b/demo-jetty/src/main/java/net/cactusthorn/routing/demo/jetty/resource/SimpleResource.java index 96092ce..a4640ff 100644 --- a/demo-jetty/src/main/java/net/cactusthorn/routing/demo/jetty/resource/SimpleResource.java +++ b/demo-jetty/src/main/java/net/cactusthorn/routing/demo/jetty/resource/SimpleResource.java @@ -15,6 +15,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.SecurityContext; @@ -69,4 +70,9 @@ public String principal(@Context SecurityContext securityContext) { @GET @RolesAllowed({ "WrongRole" }) @Path("/wrongrole") // public void wrongRole() { } + + @GET @Path("/headers") // + public String headers(@Context HttpHeaders headers) { + return headers.getHeaderString(HttpHeaders.USER_AGENT); + } } diff --git a/demo-jetty/src/main/resources/thymeleaf/index.html b/demo-jetty/src/main/resources/thymeleaf/index.html index e9e5e55..afa38bc 100644 --- a/demo-jetty/src/main/resources/thymeleaf/index.html +++ b/demo-jetty/src/main/resources/thymeleaf/index.html @@ -40,6 +40,8 @@ Security Context

@UserRoles -> Forbidden

+ + HttpHeaders

\ No newline at end of file