/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.elasticsearch.common.xcontent;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.unmodifiableMap;
import static java.util.Objects.requireNonNull;
public class NamedXContentRegistry {
/**
* The empty {@link NamedXContentRegistry} for use when you are sure that you aren't going to call
* {@link XContentParser#namedObject(Class, String, Object)}. Be *very* careful with this singleton because a parser using it will fail
* every call to {@linkplain XContentParser#namedObject(Class, String, Object)}. Every non-test usage really should be checked
* thoroughly and marked with a comment about how it was checked. That way anyone that sees code that uses it knows that it is
* potentially dangerous.
*/
public static final NamedXContentRegistry EMPTY = new NamedXContentRegistry(emptyList());
/**
* An entry in the {@linkplain NamedXContentRegistry} containing the name of the object and the parser that can parse it.
*/
public static class Entry {
/** The class that this entry can read. */
public final Class<?> categoryClass;
/** A name for the entry which is unique within the {@link #categoryClass}. */
public final ParseField name;
/** A parser capability of parser the entry's class. */
private final ContextParser<Object, ?> parser;
/** Creates a new entry which can be stored by the registry. */
public <T> Entry(Class<T> categoryClass, ParseField name, CheckedFunction<XContentParser, ? extends T, IOException> parser) {
this.categoryClass = Objects.requireNonNull(categoryClass);
this.name = Objects.requireNonNull(name);
this.parser = Objects.requireNonNull((p, c) -> parser.apply(p));
}
/**
* Creates a new entry which can be stored by the registry.
* Prefer {@link Entry#Entry(Class, ParseField, CheckedFunction)} unless you need a context to carry around while parsing.
*/
public <T> Entry(Class<T> categoryClass, ParseField name, ContextParser<Object, ? extends T> parser) {
this.categoryClass = Objects.requireNonNull(categoryClass);
this.name = Objects.requireNonNull(name);
this.parser = Objects.requireNonNull(parser);
}
}
private final Map<Class<?>, Map<String, Entry>> registry;
public NamedXContentRegistry(List<Entry> entries) {
if (entries.isEmpty()) {
registry = emptyMap();
return;
}
entries = new ArrayList<>(entries);
entries.sort((e1, e2) -> e1.categoryClass.getName().compareTo(e2.categoryClass.getName()));
Map<Class<?>, Map<String, Entry>> registry = new HashMap<>();
Map<String, Entry> parsers = null;
Class<?> currentCategory = null;
for (Entry entry : entries) {
if (currentCategory != entry.categoryClass) {
if (currentCategory != null) {
// we've seen the last of this category, put it into the big map
registry.put(currentCategory, unmodifiableMap(parsers));
}
parsers = new HashMap<>();
currentCategory = entry.categoryClass;
}
for (String name : entry.name.getAllNamesIncludedDeprecated()) {
Object old = parsers.put(name, entry);
if (old != null) {
throw new IllegalArgumentException("NamedXContent [" + currentCategory.getName() + "][" + entry.name + "]" +
" is already registered for [" + old.getClass().getName() + "]," +
" cannot register [" + entry.parser.getClass().getName() + "]");
}
}
}
// handle the last category
registry.put(currentCategory, unmodifiableMap(parsers));
this.registry = unmodifiableMap(registry);
}
/**
* Parse a named object, throwing an exception if the parser isn't found. Throws an {@link ElasticsearchException} if the
* {@code categoryClass} isn't registered because this is almost always a bug. Throws a {@link UnknownNamedObjectException} if the
* {@code categoryClass} is registered but the {@code name} isn't.
*/
public <T, C> T parseNamedObject(Class<T> categoryClass, String name, XContentParser parser, C context) throws IOException {
Map<String, Entry> parsers = registry.get(categoryClass);
if (parsers == null) {
if (registry.isEmpty()) {
// The "empty" registry will never work so we throw a better exception as a hint.
throw new ElasticsearchException("namedObject is not supported for this parser");
}
throw new ElasticsearchException("Unknown namedObject category [" + categoryClass.getName() + "]");
}
Entry entry = parsers.get(name);
if (entry == null) {
throw new UnknownNamedObjectException(parser.getTokenLocation(), categoryClass, name);
}
if (false == entry.name.match(name)) {
/* Note that this shouldn't happen because we already looked up the entry using the names but we need to call `match` anyway
* because it is responsible for logging deprecation warnings. */
throw new ParsingException(parser.getTokenLocation(),
"Unknown " + categoryClass.getSimpleName() + " [" + name + "]: Parser didn't match");
}
return categoryClass.cast(entry.parser.parse(parser, context));
}
/**
* Thrown when {@link NamedXContentRegistry#parseNamedObject(Class, String, XContentParser, Object)} is called with an unregistered
* name. When this bubbles up to the rest layer it is converted into a response with {@code 400 BAD REQUEST} status.
*/
public static class UnknownNamedObjectException extends ParsingException {
private final String categoryClass;
private final String name;
public UnknownNamedObjectException(XContentLocation contentLocation, Class<?> categoryClass,
String name) {
super(contentLocation, "Unknown " + categoryClass.getSimpleName() + " [" + name + "]");
this.categoryClass = requireNonNull(categoryClass, "categoryClass is required").getName();
this.name = requireNonNull(name, "name is required");
}
/**
* Read from a stream.
*/
public UnknownNamedObjectException(StreamInput in) throws IOException {
super(in);
categoryClass = in.readString();
name = in.readString();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(categoryClass);
out.writeString(name);
}
/**
* Category class that was missing a parser. This is a String instead of a class because the class might not be on the classpath
* of all nodes or it might be exclusive to a plugin or something.
*/
public String getCategoryClass() {
return categoryClass;
}
/**
* Name of the missing parser.
*/
public String getName() {
return name;
}
}
}