blob: 5ed717d9a39195c6d99f4a5478a5f4b70932d6ba [file] [log] [blame]
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
// * with the License. You may obtain a copy of the License at *
// * *
// * http://www.apache.org/licenses/LICENSE-2.0 *
// * *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
// * specific language governing permissions and limitations under the License. *
// ***************************************************************************************************************************
package org.apache.juneau.parser;
import static org.apache.juneau.internal.CollectionUtils.*;
import java.util.*;
import java.util.concurrent.*;
import org.apache.juneau.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.http.*;
/**
* Represents a group of {@link Parser Parsers} that can be looked up by media type.
*
* <h5 class='topic'>Description</h5>
*
* Provides the following features:
* <ul class='spaced-list'>
* <li>
* Finds parsers based on HTTP <c>Content-Type</c> header values.
* <li>
* Sets common properties on all parsers in a single method call.
* <li>
* Locks all parsers in a single method call.
* <li>
* Clones existing groups and all parsers within the group in a single method call.
* </ul>
*
* <h5 class='topic'>Match ordering</h5>
*
* Parsers are matched against <c>Content-Type</c> strings in the order they exist in this group.
*
* <p>
* Adding new entries will cause the entries to be prepended to the group.
* This allows for previous parsers to be overridden through subsequent calls.
*
* <p>
* For example, calling <code>g.append(P1.<jk>class</jk>,P2.<jk>class</jk>).append(P3.<jk>class</jk>,P4.<jk>class</jk>)</code>
* will result in the order <c>P3, P4, P1, P2</c>.
*
* <h5 class='section'>Example:</h5>
* <p class='bcode w800'>
* <jc>// Construct a new parser group builder</jc>
* ParserGroupBuilder b = ParserGroup.<jsm>create</jsm>();
*
* <jc>// Add some parsers to it</jc>
* b.append(JsonParser.<jk>class</jk>, XmlParser.<jk>class</jk>);
*
* <jc>// Change settings on parsers simultaneously</jc>
* b.set(BeanContext.<jsf>BEAN_beansRequireSerializable</jsf>, <jk>true</jk>)
* .pojoSwaps(TemporalCalendarSwap.IsoLocalDateTime.<jk>class</jk>);
*
* ParserGroup g = b.build();
*
* <jc>// Find the appropriate parser by Content-Type</jc>
* ReaderParser p = (ReaderParser)g.getParser(<js>"text/json"</js>);
*
* <jc>// Parse a bean from JSON</jc>
* String json = <js>"{...}"</js>;
* AddressBook addressBook = p.parse(json, AddressBook.<jk>class</jk>);
* </p>
*/
@ConfigurableContext(nocache=true)
public final class ParserGroup extends BeanContext {
/**
* An unmodifiable empty parser group.
*/
public static final ParserGroup EMPTY = create().build();
// Maps Content-Type headers to matches.
private final ConcurrentHashMap<String,ParserMatch> cache = new ConcurrentHashMap<>();
private final MediaType[] mediaTypes; // List of media types
private final List<MediaType> mediaTypesList;
private final Parser[] mediaTypeParsers;
private final List<Parser> parsers;
/**
* Instantiates a new clean-slate {@link ParserGroupBuilder} object.
*
* <p>
* This is equivalent to simply calling <code><jk>new</jk> ParserGroupBuilder()</code>.
*
* @return A new {@link ParserGroupBuilder} object.
*/
public static ParserGroupBuilder create() {
return new ParserGroupBuilder();
}
/**
* Returns a builder that's a copy of the settings on this parser group.
*
* @return A new {@link ParserGroupBuilder} initialized to this group.
*/
@Override /* Context */
public ParserGroupBuilder builder() {
return new ParserGroupBuilder(this);
}
/**
* Constructor.
*
* @param ps
* The modifiable properties that were used to initialize the parsers.
* A snapshot of these will be made so that we can clone and modify this group.
* @param parsers
* The parsers defined in this group.
* The order is important because they will be tried in reverse order (e.g. newer first) in which they will be
* tried to match against media types.
*/
public ParserGroup(PropertyStore ps, Parser[] parsers) {
super(ps);
this.parsers = immutableList(parsers);
List<MediaType> lmt = new ArrayList<>();
List<Parser> l = new ArrayList<>();
for (Parser p : parsers) {
for (MediaType m: p.getMediaTypes()) {
lmt.add(m);
l.add(p);
}
}
this.mediaTypes = lmt.toArray(new MediaType[lmt.size()]);
this.mediaTypesList = unmodifiableList(lmt);
this.mediaTypeParsers = l.toArray(new Parser[l.size()]);
}
/**
* Searches the group for a parser that can handle the specified <l>Content-Type</l> header value.
*
* <p>
* The returned object includes both the parser and media type that matched.
*
* @param contentTypeHeader The HTTP <l>Content-Type</l> header value.
* @return The parser and media type that matched the content type header, or <jk>null</jk> if no match was made.
*/
public ParserMatch getParserMatch(String contentTypeHeader) {
ParserMatch pm = cache.get(contentTypeHeader);
if (pm != null)
return pm;
ContentType ct = ContentType.forString(contentTypeHeader);
int match = ct.findMatch(mediaTypes);
if (match >= 0) {
pm = new ParserMatch(mediaTypes[match], mediaTypeParsers[match]);
cache.putIfAbsent(contentTypeHeader, pm);
}
return cache.get(contentTypeHeader);
}
/**
* Same as {@link #getParserMatch(String)} but matches using a {@link MediaType} instance.
*
* @param mediaType The HTTP <l>Content-Type</l> header value as a media type.
* @return The parser and media type that matched the media type, or <jk>null</jk> if no match was made.
*/
public ParserMatch getParserMatch(MediaType mediaType) {
return getParserMatch(mediaType.toString());
}
/**
* Same as {@link #getParserMatch(String)} but returns just the matched parser.
*
* @param contentTypeHeader The HTTP <l>Content-Type</l> header string.
* @return The parser that matched the content type header, or <jk>null</jk> if no match was made.
*/
public Parser getParser(String contentTypeHeader) {
ParserMatch pm = getParserMatch(contentTypeHeader);
return pm == null ? null : pm.getParser();
}
/**
* Same as {@link #getParserMatch(MediaType)} but returns just the matched parser.
*
* @param mediaType The HTTP media type.
* @return The parser that matched the media type, or <jk>null</jk> if no match was made.
*/
public Parser getParser(MediaType mediaType) {
ParserMatch pm = getParserMatch(mediaType);
return pm == null ? null : pm.getParser();
}
/**
* Returns the media types that all parsers in this group can handle
*
* <p>
* Entries are ordered in the same order as the parsers in the group.
*
* @return An unmodifiable list of media types.
*/
public List<MediaType> getSupportedMediaTypes() {
return mediaTypesList;
}
/**
* Returns the parsers in this group.
*
* @return An unmodifiable list of parsers in this group.
*/
public List<Parser> getParsers() {
return parsers;
}
}