blob: fbdc01508bbd5fb09316c2cd5c0e42cb2a7a8da3 [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.solr.common.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Objects;
import org.apache.solr.common.NavigableObject;
import org.apache.solr.common.SolrException;
import org.noggit.JSONParser;
import org.noggit.ObjectBuilder;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableSet;
public class ValidatingJsonMap implements Map<String, Object>, NavigableObject {
private static final String INCLUDE = "#include";
private static final String RESOURCE_EXTENSION = ".json";
public static final PredicateWithErrMsg<Object> NOT_NULL = o -> {
if (o == null) return " Must not be NULL";
return null;
};
@SuppressWarnings({"rawtypes"})
public static final PredicateWithErrMsg<Pair> ENUM_OF = pair -> {
if (pair.second() instanceof Set) {
Set set = (Set) pair.second();
if (pair.first() instanceof Collection) {
for (Object o : (Collection) pair.first()) {
if (!set.contains(o)) {
return " Must be one of " + pair.second();
}
}
} else {
if (!set.contains(pair.first())) return " Must be one of " + pair.second() + ", got " + pair.first();
}
return null;
} else {
return " Unknown type";
}
};
private final Map<String, Object> delegate;
public ValidatingJsonMap(Map<String, Object> delegate) {
this.delegate = delegate;
}
public ValidatingJsonMap(int i) {
delegate = new LinkedHashMap<>(i);
}
public ValidatingJsonMap() {
delegate = new LinkedHashMap<>();
}
@Override
public int size() {
return delegate.size();
}
@Override
public boolean isEmpty() {
return delegate.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return delegate.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return delegate.containsValue(value);
}
@Override
public Object get(Object key) {
return delegate.get(key);
}
@Override
public Object put(String key, Object value) {
return delegate.put(key, value);
}
@Override
public Object remove(Object key) {
return delegate.remove(key);
}
@Override
public void putAll(Map<? extends String, ?> m) {
delegate.putAll(m);
}
@Override
public void clear() {
delegate.clear();
}
@Override
public Set<String> keySet() {
return delegate.keySet();
}
@Override
public Collection<Object> values() {
return delegate.values();
}
@Override
public Set<Entry<String, Object>> entrySet() {
return delegate.entrySet();
}
@SuppressWarnings({"unchecked"})
public Object get(String key, @SuppressWarnings({"rawtypes"})PredicateWithErrMsg predicate) {
Object v = get(key);
if (predicate != null) {
String msg = predicate.test(v);
if (msg != null) {
throw new RuntimeException("" + key + msg);
}
}
return v;
}
public Boolean getBool(String key, Boolean def) {
Object v = get(key);
if (v == null) return def;
if (v instanceof Boolean) return (Boolean) v;
try {
return Boolean.parseBoolean(v.toString());
} catch (NumberFormatException e) {
throw new RuntimeException("value of " + key + "must be an boolean");
}
}
public Integer getInt(String key, Integer def) {
Object v = get(key);
if (v == null) return def;
if (v instanceof Integer) return (Integer) v;
try {
return Integer.parseInt(v.toString());
} catch (NumberFormatException e) {
throw new RuntimeException("value of " + key + "must be an integer");
}
}
public ValidatingJsonMap getMap(String key) {
return getMap(key, null, null);
}
public ValidatingJsonMap getMap(String key, @SuppressWarnings({"rawtypes"})PredicateWithErrMsg predicate) {
return getMap(key, predicate, null);
}
@SuppressWarnings({"unchecked", "rawtypes"})
public ValidatingJsonMap getMap(String key, PredicateWithErrMsg predicate, String message) {
Object v = get(key);
if (v != null && !(v instanceof Map)) {
throw new RuntimeException("" + key + " should be of type map");
}
if (predicate != null) {
String msg = predicate.test(v);
if (msg != null) {
msg = message != null ? message : key + msg;
throw new RuntimeException(msg);
}
}
return wrap((Map) v);
}
@SuppressWarnings({"rawtypes"})
public List getList(String key, PredicateWithErrMsg predicate) {
return getList(key, predicate, null);
}
@SuppressWarnings({"unchecked", "rawtypes"})
public List getList(String key, PredicateWithErrMsg predicate, Object test) {
Object v = get(key);
if (v != null && !(v instanceof List)) {
throw new RuntimeException("" + key + " should be of type List");
}
if (predicate != null) {
String msg = predicate.test(test == null ? v : new Pair(v, test));
if (msg != null) {
throw new RuntimeException("" + key + msg);
}
}
return (List) v;
}
@SuppressWarnings({"unchecked", "rawtypes"})
public Object get(String key, PredicateWithErrMsg<Pair> predicate, Object arg) {
Object v = get(key);
String test = predicate.test(new Pair(v, arg));
if (test != null) {
throw new RuntimeException("" + key + test);
}
return v;
}
public Object get(String k, Object def) {
Object v = get(k);
if (v == null) return def;
return v;
}
static ValidatingJsonMap wrap(Map<String, Object> map) {
if (map == null) return null;
if (map instanceof ValidatingJsonMap) {
return (ValidatingJsonMap) map;
} else {
return new ValidatingJsonMap(map);
}
}
public static ValidatingJsonMap fromJSON(InputStream is, String includeLocation) {
return fromJSON(new InputStreamReader(is, UTF_8), includeLocation);
}
public static ValidatingJsonMap fromJSON(Reader s, String includeLocation) {
try {
ValidatingJsonMap map = (ValidatingJsonMap) getObjectBuilder(new JSONParser(s)).getObject();
handleIncludes(map, includeLocation, 4);
return map;
} catch (IOException e) {
throw new RuntimeException();
}
}
/**
* In the given map, recursively replace "#include":"resource-name" with the key/value pairs
* parsed from the resource at {location}/{resource-name}.json
*/
private static void handleIncludes(ValidatingJsonMap map, String location, int maxDepth) {
final String loc = location == null ? "" // trim trailing slash
: (location.endsWith("/") ? location.substring(0, location.length() - 1) : location);
String resourceToInclude = (String) map.get(INCLUDE);
if (resourceToInclude != null) {
ValidatingJsonMap includedMap = parse(loc + "/" + resourceToInclude + RESOURCE_EXTENSION, loc);
map.remove(INCLUDE);
map.putAll(includedMap);
}
if (maxDepth > 0) {
map.values().stream()
.filter(o -> o instanceof Map)
.forEach(m -> handleIncludes((ValidatingJsonMap) m, loc, maxDepth - 1));
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
public static ValidatingJsonMap getDeepCopy(Map map, int maxDepth, boolean mutable) {
if (map == null) return null;
if (maxDepth < 1) return ValidatingJsonMap.wrap(map);
ValidatingJsonMap copy = mutable ? new ValidatingJsonMap(map.size()) : new ValidatingJsonMap();
for (Object o : map.entrySet()) {
Map.Entry<String, Object> e = (Entry<String, Object>) o;
Object v = e.getValue();
if (v instanceof Map) v = getDeepCopy((Map) v, maxDepth - 1, mutable);
else if (v instanceof Collection) v = getDeepCopy((Collection) v, maxDepth - 1, mutable);
copy.put(e.getKey(), v);
}
return mutable ? copy : new ValidatingJsonMap(Collections.unmodifiableMap(copy));
}
@SuppressWarnings({"unchecked", "rawtypes"})
public static Collection getDeepCopy(Collection c, int maxDepth, boolean mutable) {
if (c == null || maxDepth < 1) return c;
Collection result = c instanceof Set ? new HashSet() : new ArrayList();
for (Object o : c) {
if (o instanceof Map) {
o = getDeepCopy((Map) o, maxDepth - 1, mutable);
}
result.add(o);
}
return mutable ? result : result instanceof Set ? unmodifiableSet((Set) result) : unmodifiableList((List) result);
}
private static ObjectBuilder getObjectBuilder(final JSONParser jp) throws IOException {
return new ObjectBuilder(jp) {
@Override
public Object newObject() throws IOException {
return new ValidatingJsonMap();
}
};
}
public static ValidatingJsonMap parse(String resourceName, String includeLocation) {
final URL resource = ValidatingJsonMap.class.getClassLoader().getResource(resourceName);
if (null == resource) {
throw new RuntimeException("invalid API spec: " + resourceName);
}
ValidatingJsonMap map = null;
try (InputStream is = resource.openStream()) {
try {
map = fromJSON(is, includeLocation);
} catch (Exception e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error in JSON : " + resourceName, e);
}
} catch (IOException ioe) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
"Unable to read resource: " + resourceName, ioe);
}
if (map == null) throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Empty value for " + resourceName);
return getDeepCopy(map, 5, false);
}
@Override
public boolean equals(Object that) {
return that instanceof Map && this.delegate.equals(that);
}
@Override
public int hashCode() {
return Objects.hash(delegate);
}
@SuppressWarnings({"unchecked"})
public static final ValidatingJsonMap EMPTY = new ValidatingJsonMap(Collections.EMPTY_MAP);
public interface PredicateWithErrMsg<T> {
/**
* Test the object and return null if the predicate is true
* or return a string with a message;
*
* @param t test value
* @return null if test succeeds or an error description if test fails
*/
String test(T t);
}
}