001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.parser; 014 015import java.util.*; 016import java.util.concurrent.*; 017 018import org.apache.juneau.*; 019import org.apache.juneau.annotation.*; 020import org.apache.juneau.collections.*; 021import org.apache.juneau.http.*; 022import org.apache.juneau.http.header.*; 023 024/** 025 * Represents a group of {@link Parser Parsers} that can be looked up by media type. 026 * 027 * <h5 class='topic'>Description</h5> 028 * 029 * Provides the following features: 030 * <ul class='spaced-list'> 031 * <li> 032 * Finds parsers based on HTTP <c>Content-Type</c> header values. 033 * <li> 034 * Sets common properties on all parsers in a single method call. 035 * <li> 036 * Locks all parsers in a single method call. 037 * <li> 038 * Clones existing groups and all parsers within the group in a single method call. 039 * </ul> 040 * 041 * <h5 class='topic'>Match ordering</h5> 042 * 043 * Parsers are matched against <c>Content-Type</c> strings in the order they exist in this group. 044 * 045 * <p> 046 * Adding new entries will cause the entries to be prepended to the group. 047 * This allows for previous parsers to be overridden through subsequent calls. 048 * 049 * <p> 050 * For example, calling <code>g.append(P1.<jk>class</jk>,P2.<jk>class</jk>).append(P3.<jk>class</jk>,P4.<jk>class</jk>)</code> 051 * will result in the order <c>P3, P4, P1, P2</c>. 052 * 053 * <h5 class='section'>Example:</h5> 054 * <p class='bcode w800'> 055 * <jc>// Construct a new parser group builder</jc> 056 * ParserGroupBuilder b = ParserGroup.<jsm>create</jsm>(); 057 * 058 * <jc>// Add some parsers to it</jc> 059 * b.append(JsonParser.<jk>class</jk>, XmlParser.<jk>class</jk>); 060 * 061 * <jc>// Change settings on parsers simultaneously</jc> 062 * b.set(BeanContext.<jsf>BEAN_beansRequireSerializable</jsf>, <jk>true</jk>) 063 * .swaps(TemporalCalendarSwap.IsoLocalDateTime.<jk>class</jk>); 064 * 065 * ParserGroup g = b.build(); 066 * 067 * <jc>// Find the appropriate parser by Content-Type</jc> 068 * ReaderParser p = (ReaderParser)g.getParser(<js>"text/json"</js>); 069 * 070 * <jc>// Parse a bean from JSON</jc> 071 * String json = <js>"{...}"</js>; 072 * AddressBook addressBook = p.parse(json, AddressBook.<jk>class</jk>); 073 * </p> 074 */ 075@ConfigurableContext(nocache=true) 076public final class ParserGroup extends BeanContext { 077 078 /** 079 * An unmodifiable empty parser group. 080 */ 081 public static final ParserGroup EMPTY = create().build(); 082 083 // Maps Content-Type headers to matches. 084 private final ConcurrentHashMap<String,ParserMatch> cache = new ConcurrentHashMap<>(); 085 086 private final List<MediaType> mediaTypes; 087 private final List<Parser> mediaTypeParsers; 088 private final List<Parser> parsers; 089 090 /** 091 * Instantiates a new clean-slate {@link ParserGroupBuilder} object. 092 * 093 * <p> 094 * This is equivalent to simply calling <code><jk>new</jk> ParserGroupBuilder()</code>. 095 * 096 * @return A new {@link ParserGroupBuilder} object. 097 */ 098 public static ParserGroupBuilder create() { 099 return new ParserGroupBuilder(); 100 } 101 102 /** 103 * Returns a builder that's a copy of the settings on this parser group. 104 * 105 * @return A new {@link ParserGroupBuilder} initialized to this group. 106 */ 107 @Override /* Context */ 108 public ParserGroupBuilder builder() { 109 return new ParserGroupBuilder(this); 110 } 111 112 /** 113 * Constructor. 114 * 115 * @param ps 116 * The modifiable properties that were used to initialize the parsers. 117 * A snapshot of these will be made so that we can clone and modify this group. 118 * @param parsers 119 * The parsers defined in this group. 120 * The order is important because they will be tried in reverse order (e.g. newer first) in which they will be 121 * tried to match against media types. 122 */ 123 public ParserGroup(PropertyStore ps, Parser[] parsers) { 124 super(ps); 125 this.parsers = AList.unmodifiable(parsers); 126 127 AList<MediaType> lmt = AList.of(); 128 AList<Parser> l = AList.of(); 129 for (Parser p : parsers) { 130 for (MediaType m: p.getMediaTypes()) { 131 lmt.add(m); 132 l.add(p); 133 } 134 } 135 136 this.mediaTypes = lmt.unmodifiable(); 137 this.mediaTypeParsers = l.unmodifiable(); 138 } 139 140 /** 141 * Searches the group for a parser that can handle the specified <l>Content-Type</l> header value. 142 * 143 * <p> 144 * The returned object includes both the parser and media type that matched. 145 * 146 * @param contentTypeHeader The HTTP <l>Content-Type</l> header value. 147 * @return The parser and media type that matched the content type header, or <jk>null</jk> if no match was made. 148 */ 149 public ParserMatch getParserMatch(String contentTypeHeader) { 150 ParserMatch pm = cache.get(contentTypeHeader); 151 if (pm != null) 152 return pm; 153 154 ContentType ct = ContentType.of(contentTypeHeader); 155 int match = ct.match(mediaTypes); 156 157 if (match >= 0) { 158 pm = new ParserMatch(mediaTypes.get(match), mediaTypeParsers.get(match)); 159 cache.putIfAbsent(contentTypeHeader, pm); 160 } 161 162 return cache.get(contentTypeHeader); 163 } 164 165 /** 166 * Same as {@link #getParserMatch(String)} but matches using a {@link MediaType} instance. 167 * 168 * @param mediaType The HTTP <l>Content-Type</l> header value as a media type. 169 * @return The parser and media type that matched the media type, or <jk>null</jk> if no match was made. 170 */ 171 public ParserMatch getParserMatch(MediaType mediaType) { 172 return getParserMatch(mediaType.toString()); 173 } 174 175 /** 176 * Same as {@link #getParserMatch(String)} but returns just the matched parser. 177 * 178 * @param contentTypeHeader The HTTP <l>Content-Type</l> header string. 179 * @return The parser that matched the content type header, or <jk>null</jk> if no match was made. 180 */ 181 public Parser getParser(String contentTypeHeader) { 182 ParserMatch pm = getParserMatch(contentTypeHeader); 183 return pm == null ? null : pm.getParser(); 184 } 185 186 /** 187 * Same as {@link #getParserMatch(MediaType)} but returns just the matched parser. 188 * 189 * @param mediaType The HTTP media type. 190 * @return The parser that matched the media type, or <jk>null</jk> if no match was made. 191 */ 192 public Parser getParser(MediaType mediaType) { 193 ParserMatch pm = getParserMatch(mediaType); 194 return pm == null ? null : pm.getParser(); 195 } 196 197 /** 198 * Returns the media types that all parsers in this group can handle 199 * 200 * <p> 201 * Entries are ordered in the same order as the parsers in the group. 202 * 203 * @return An unmodifiable list of media types. 204 */ 205 public List<MediaType> getSupportedMediaTypes() { 206 return mediaTypes; 207 } 208 209 /** 210 * Returns the parsers in this group. 211 * 212 * @return An unmodifiable list of parsers in this group. 213 */ 214 public List<Parser> getParsers() { 215 return parsers; 216 } 217 218 /** 219 * Returns <jk>true</jk> if this group contains no parsers. 220 * 221 * @return <jk>true</jk> if this group contains no parsers. 222 */ 223 public boolean isEmpty() { 224 return parsers.isEmpty(); 225 } 226}