SOLR-13841: removed jackson dependencies from SolrJ and provided a mapping to our annotation (#992)

Provide mappings for jackson annotation @JsonProperty to use Jackson deserializer
This commit is contained in:
Noble Paul 2019-11-04 16:38:21 +11:00 committed by GitHub
parent 7f7730d6bc
commit b5f5b0f2bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 336 additions and 70 deletions

View File

@ -153,7 +153,7 @@ Other Changes
* SOLR-12769: Fix incorrect documentation for 'delete' op in Request parameters API (Alexandre Rafalovitch, Munendra S N)
* SOLR-13841: Add jackson databind annotations to SolrJ classpath (noble)
* Provide mappings for jackson annotation @JsonProperty to use Jackson deserializer (noble)
* SOLR-13824: Strictly reject anything after JSON in most APIs (Mikhail Khludnev, Munendra S N)

View File

@ -19,7 +19,6 @@ package org.apache.solr.api;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@ -33,18 +32,19 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SpecProvider;
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.JsonSchemaCreator;
import org.apache.solr.common.util.Utils;
import org.apache.solr.common.util.ValidatingJsonMap;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.PermissionNameProvider;
import org.apache.solr.util.SolrJacksonAnnotationInspector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -61,7 +61,7 @@ import org.slf4j.LoggerFactory;
public class AnnotatedApi extends Api implements PermissionNameProvider {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String ERR ="Error executing commands :";
public static final String ERR = "Error executing commands :";
private EndPoint endPoint;
private Map<String, Cmd> commands = new HashMap<>();
private final Api fallback;
@ -137,7 +137,7 @@ public class AnnotatedApi extends Api implements PermissionNameProvider {
}
}
List<CommandOperation> cmds = req.getCommands(false);
List<CommandOperation> cmds = req.getCommands(true);
boolean allExists = true;
for (CommandOperation cmd : cmds) {
if (!commands.containsKey(cmd.name)) {
@ -161,8 +161,8 @@ public class AnnotatedApi extends Api implements PermissionNameProvider {
List<Map> errs = CommandOperation.captureErrors(cmds);
if (!errs.isEmpty()) {
log.error(ERR+ Utils.toJSONString(errs));
throw new ApiBag.ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, ERR , errs);
log.error(ERR + Utils.toJSONString(errs));
throw new ApiBag.ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, ERR, errs);
}
}
@ -178,6 +178,7 @@ public class AnnotatedApi extends Api implements PermissionNameProvider {
Cmd(Command command, Object obj, Method method) {
mapper.setAnnotationIntrospector(SolrJacksonAnnotationInspector.INSTANCE);
if (Modifier.isPublic(method.getModifiers())) {
this.command = command;
this.obj = obj;
@ -253,61 +254,20 @@ public class AnnotatedApi extends Api implements PermissionNameProvider {
}
}
private static final Map<Class, String> primitives = new HashMap<>();
static {
primitives.put(String.class, "string");
primitives.put(Integer.class, "integer");
primitives.put(int.class, "integer");
primitives.put(Float.class, "number");
primitives.put(float.class, "number");
primitives.put(Double.class, "number");
primitives.put(double.class, "number");
primitives.put(Boolean.class, "boolean");
primitives.put(List.class, "array");
}
public static Map<String, Object> createSchema(Method m) {
Type[] types = m.getGenericParameterTypes();
if (types.length == 3) {
return createSchemaFromType(types[2]);
Type t = types[2];
if (t instanceof ParameterizedType) {
ParameterizedType typ = (ParameterizedType) t;
if (typ.getRawType() == PayloadObj.class) {
t = typ.getActualTypeArguments()[0];
}
}
return JsonSchemaCreator.getSchema(t);
}
return null;
}
private static Map<String, Object> createSchemaFromType(Type t) {
Map<String, Object> map = new LinkedHashMap<>();
if (t instanceof ParameterizedType) {
ParameterizedType typ = (ParameterizedType) t;
if (typ.getRawType() == PayloadObj.class) {
t = typ.getActualTypeArguments()[0];
}
}
if (primitives.containsKey(t)) {
map.put("type", primitives.get(t));
} else if (t instanceof ParameterizedType && ((ParameterizedType) t).getRawType() == List.class) {
Type typ = ((ParameterizedType) t).getActualTypeArguments()[0];
map.put("type", "array");
map.put("items", createSchemaFromType(typ));
} else {
createObjectSchema((Class) t, map);
}
return map;
}
private static void createObjectSchema(Class klas, Map<String, Object> map) {
map.put("type", "object");
Map<String, Object> props = new HashMap<>();
map.put("properties", props);
for (Field fld : klas.getDeclaredFields()) {
JsonProperty p = fld.getAnnotation(JsonProperty.class);
if (p == null) continue;
props.put(p.value(), createSchemaFromType(fld.getGenericType()));
}
}
}

View File

@ -26,7 +26,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.solr.api.Command;
import org.apache.solr.api.EndPoint;
@ -34,6 +33,7 @@ import org.apache.solr.api.PayloadObj;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.request.beans.Package;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.cloud.ZooKeeperException;
@ -44,6 +44,7 @@ import org.apache.solr.core.CoreContainer;
import org.apache.solr.filestore.PackageStoreAPI;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.util.SolrJacksonAnnotationInspector;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
@ -55,13 +56,16 @@ import static org.apache.solr.common.cloud.ZkStateReader.SOLR_PKGS_PATH;
import static org.apache.solr.security.PermissionNameProvider.Name.PACKAGE_EDIT_PERM;
import static org.apache.solr.security.PermissionNameProvider.Name.PACKAGE_READ_PERM;
/**This implements the public end points (/api/cluster/package) of package API.
*
*/
public class PackageAPI {
public static final String PACKAGES = "packages";
public final boolean enablePackages = Boolean.parseBoolean(System.getProperty("enable.packages", "false"));
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
final CoreContainer coreContainer;
private ObjectMapper mapper = new ObjectMapper();
private final ObjectMapper mapper = new ObjectMapper();
private final PackageLoader packageLoader;
Packages pkgs;
@ -71,6 +75,7 @@ public class PackageAPI {
public PackageAPI(CoreContainer coreContainer, PackageLoader loader) {
this.coreContainer = coreContainer;
this.packageLoader = loader;
mapper.setAnnotationIntrospector(SolrJacksonAnnotationInspector.INSTANCE);
pkgs = new Packages();
SolrZkClient zkClient = coreContainer.getZkController().getZkClient();
try {
@ -243,7 +248,7 @@ public class PackageAPI {
packages = new Packages();
}
packages.packages.computeIfAbsent(add.pkg, Utils.NEW_ARRAYLIST_FUN).add(new PkgVersion(add));
packages.znodeVersion = stat.getVersion() + 1;
packages.znodeVersion = stat.getVersion() ;
finalState[0] = packages;
return Utils.toJSON(packages);
});

View File

@ -0,0 +1,89 @@
/*
* 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.util;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.PropertyName;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.util.BeanUtil;
import org.apache.solr.common.annotation.JsonProperty;
/**this class provides a mapping between Solr's {@link JsonProperty} annotation to a corresponding annotation
* in jackson
see SOLR-13841 for more details
* <blockquote><pre>
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new SolrJacksonAnnotationInspector());
* </pre></blockquote>
**/
public class SolrJacksonAnnotationInspector extends AnnotationIntrospector {
public static final SolrJacksonAnnotationInspector INSTANCE = new SolrJacksonAnnotationInspector();
@Override
public Version version() {
return Version.unknownVersion();
}
@Override
public PropertyName findNameForSerialization(Annotated a) {
if (a instanceof AnnotatedMethod) {
AnnotatedMethod am = (AnnotatedMethod) a;
JsonProperty prop = am.getAnnotation(JsonProperty.class);
if (prop == null) return null;
if (prop.value().isEmpty()) {
return new PropertyName(BeanUtil.okNameForGetter(am, true));
} else {
return new PropertyName(prop.value());
}
}
if (a instanceof AnnotatedField) {
AnnotatedField af = (AnnotatedField) a;
JsonProperty prop = af.getAnnotation(JsonProperty.class);
if (prop == null) return null;
return prop.value().isEmpty() ?
new PropertyName(af.getName()) :
new PropertyName(prop.value());
}
return null;
}
@Override
public Boolean hasRequiredMarker(AnnotatedMember m) {
JsonProperty prop = m.getAnnotation(JsonProperty.class);
if (prop == null) return Boolean.FALSE;
return prop.required();
}
@Override
public String findPropertyDefaultValue(Annotated m) {
JsonProperty prop = m.getAnnotation(JsonProperty.class);
if (prop == null) return "";
return prop.defaultValue();
}
@Override
public PropertyName findNameForDeserialization(Annotated a) {
return findNameForSerialization(a);
}
}

View File

@ -27,7 +27,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.api.AnnotatedApi;
import org.apache.solr.api.Api;
@ -37,6 +36,7 @@ import org.apache.solr.api.EndPoint;
import org.apache.solr.api.V2HttpCall;
import org.apache.solr.api.V2HttpCall.CompositeApi;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;

View File

@ -0,0 +1,82 @@
/*
* 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.util;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.solr.SolrTestCase;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.util.JsonSchemaCreator;
import org.apache.solr.common.util.JsonSchemaValidator;
import org.apache.solr.common.util.Utils;
public class TestSolrJacksonAnnotation extends SolrTestCase {
public void testSerDe() throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new SolrJacksonAnnotationInspector());
TestObj o = new TestObj();
o.field = "v1";
o.f2 = "v2";
o.ifld = 1234;
String json = mapper.writeValueAsString(o);
Map m = (Map) Utils.fromJSONString(json);
assertEquals("v1", m.get("field"));
assertEquals("v2", m.get("friendlyName"));
assertEquals("1234", String.valueOf(m.get("friendlyIntFld")));
TestObj o1 = mapper.readValue(json, TestObj.class);
assertEquals("v1", o1.field);
assertEquals("v2", o1.f2);
assertEquals(1234, o1.ifld);
Map<String, Object> schema = JsonSchemaCreator.getSchema(TestObj.class);
assertEquals("string", Utils.getObjectByPath(schema,true,"/properties/friendlyName/type"));
assertEquals("integer", Utils.getObjectByPath(schema,true,"/properties/friendlyIntFld/type"));
assertEquals("friendlyName", Utils.getObjectByPath(schema,true,"/required[0]"));
JsonSchemaValidator validator = new JsonSchemaValidator(schema);
List<String> errs = validator.validateJson(m);
assertNull(errs);
m.remove("friendlyName");
errs = validator.validateJson(m);
assertFalse(errs.isEmpty());
assertTrue(errs.get(0).contains("Missing required attribute"));
m.put("friendlyIntFld", Boolean.TRUE);
errs = validator.validateJson(m);
m.put("friendlyIntFld", "another String");
assertTrue(errs.get(0).contains("Value is not valid"));
}
public static class TestObj {
@JsonProperty()
public String field;
@JsonProperty(value = "friendlyName" ,required = true)
public String f2;
@JsonProperty("friendlyIntFld")
public int ifld;
}
}

View File

@ -61,8 +61,6 @@
<dependency org="io.netty" name="netty-transport-native-epoll" rev="${/io.netty/netty-transport-native-epoll}" conf="compile"/>
<dependency org="io.netty" name="netty-transport-native-unix-common" rev="${/io.netty/netty-transport-native-unix-common}" conf="compile"/>
<dependency org="com.fasterxml.jackson.core" name="jackson-annotations" rev="${/com.fasterxml.jackson.core/jackson-annotations}" conf="compile"/>
<dependency org="org.apache.logging.log4j" name="log4j-slf4j-impl" rev="${/org.apache.logging.log4j/log4j-slf4j-impl}" conf="test"/>
<dependency org="org.mockito" name="mockito-core" rev="${/org.mockito/mockito-core}" conf="test"/>

View File

@ -37,8 +37,8 @@ import org.apache.solr.client.solrj.io.Tuple;
import org.apache.solr.client.solrj.io.comp.StreamComparator;
import org.apache.solr.client.solrj.io.eval.DbscanEvaluator;
import org.apache.solr.client.solrj.io.eval.KmeansEvaluator;
import org.apache.solr.client.solrj.io.eval.StreamEvaluator;
import org.apache.solr.client.solrj.io.eval.Matrix;
import org.apache.solr.client.solrj.io.eval.StreamEvaluator;
import org.apache.solr.client.solrj.io.stream.expr.Explanation;
import org.apache.solr.client.solrj.io.stream.expr.Explanation.ExpressionType;
import org.apache.solr.client.solrj.io.stream.expr.Expressible;

View File

@ -19,7 +19,7 @@ package org.apache.solr.client.solrj.request.beans;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.util.ReflectMapWriter;
/**Just a container class for POJOs used in Package APIs

View File

@ -16,7 +16,7 @@
*/
/**
* Data objects used in V2 Requests with jackson bindings
* Annotated Data objects used in V2 Requests
*/
package org.apache.solr.client.solrj.request.beans;

View File

@ -0,0 +1,39 @@
/*
* 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.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**Provides a 1:1 mapping from jackson's annotation. This was created to avoid direct dependency on jackson in SolrJ
*
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonProperty {
String value() default "";
boolean required() default false;
// not implemented
// int index() default -1;
String defaultValue() default "";
}

View File

@ -0,0 +1,85 @@
/*
* 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.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.solr.common.annotation.JsonProperty;
/**Creates a json schema from an annotated Java Object. This, by no means, is an exhaustive impl
* of schema generation. We will expand the scope as we use more types
*
*/
public class JsonSchemaCreator {
public static final Map<Class, String> natives = new HashMap<>();
static {
natives.put(String.class, "string");
natives.put(Integer.class, "integer");
natives.put(int.class, "integer");
natives.put(Float.class, "number");
natives.put(float.class, "number");
natives.put(Double.class, "number");
natives.put(double.class, "number");
natives.put(Boolean.class, "boolean");
natives.put(List.class, "array");
}
public static Map<String, Object> getSchema(java.lang.reflect.Type t) {
return createSchemaFromType(t, new LinkedHashMap<>());
}
private static Map<String, Object> createSchemaFromType(java.lang.reflect.Type t, Map<String, Object> map) {
if (natives.containsKey(t)) {
map.put("type", natives.get(t));
} else if (t instanceof ParameterizedType && ((ParameterizedType) t).getRawType() == List.class) {
Type typ = ((ParameterizedType) t).getActualTypeArguments()[0];
map.put("type", "array");
map.put("items", getSchema(typ));
} else {
createObjectSchema((Class) t, map);
}
return map;
}
private static void createObjectSchema(Class klas, Map<String, Object> map) {
map.put("type", "object");
Map<String, Object> props = new HashMap<>();
map.put("properties", props);
Set<String> required = new HashSet<>();
for (Field fld : klas.getDeclaredFields()) {
JsonProperty p = fld.getAnnotation(JsonProperty.class);
if (p == null) continue;
String name = p.value().isEmpty() ? fld.getName() : p.value();
props.put(name, getSchema(fld.getGenericType()));
if(p.required()) required.add(name);
}
if(!required.isEmpty()) map.put("required", new ArrayList<>(required));
}
}

View File

@ -21,8 +21,9 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.solr.common.MapWriter;
import org.apache.solr.common.annotation.JsonProperty;
// An implementation of MapWriter which is annotated with Jackson annotations
public interface ReflectMapWriter extends MapWriter {

View File

@ -21,15 +21,22 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.LuceneTestCase.Slow;
import org.apache.solr.SolrTestCase;
import org.apache.solr.client.solrj.io.eval.*;
import org.apache.solr.client.solrj.io.eval.TemporalEvaluatorDay;
import org.apache.solr.client.solrj.io.eval.TemporalEvaluatorDayOfQuarter;
import org.apache.solr.client.solrj.io.eval.TemporalEvaluatorDayOfYear;
import org.apache.solr.client.solrj.io.eval.TemporalEvaluatorEpoch;
import org.apache.solr.client.solrj.io.eval.TemporalEvaluatorHour;
import org.apache.solr.client.solrj.io.eval.TemporalEvaluatorMinute;
import org.apache.solr.client.solrj.io.eval.TemporalEvaluatorMonth;
import org.apache.solr.client.solrj.io.eval.TemporalEvaluatorQuarter;
import org.apache.solr.client.solrj.io.eval.TemporalEvaluatorSecond;
import org.apache.solr.client.solrj.io.eval.TemporalEvaluatorWeek;
import org.apache.solr.client.solrj.io.eval.TemporalEvaluatorYear;
import org.apache.solr.client.solrj.io.stream.expr.Expressible;
import org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
import org.junit.Test;
@Slow