blob: 818ddec21ca9c1a88d2807399f7ff346d6e3364c [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.johnzon.mapper.access;
import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;
import static java.util.Comparator.comparing;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toSet;
import static org.apache.johnzon.mapper.reflection.Records.isRecord;
import static org.apache.johnzon.mapper.reflection.Converters.matches;
import java.beans.ConstructorProperties;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Stream;
import org.apache.johnzon.mapper.Adapter;
import org.apache.johnzon.mapper.Converter;
import org.apache.johnzon.mapper.JohnzonAny;
import org.apache.johnzon.mapper.JohnzonConverter;
import org.apache.johnzon.mapper.JohnzonRecord;
import org.apache.johnzon.mapper.MapperConverter;
import org.apache.johnzon.mapper.ObjectConverter;
import org.apache.johnzon.mapper.internal.ConverterAdapter;
// handle some specific types
public abstract class BaseAccessMode implements AccessMode {
private static final Type[] NO_PARAMS = new Type[0];
private FieldFilteringStrategy fieldFilteringStrategy = new SingleEntryFieldFilteringStrategy();
private final boolean acceptHiddenConstructor;
private final boolean useConstructor;
protected BaseAccessMode(final boolean useConstructor, final boolean acceptHiddenConstructor) {
this.useConstructor = useConstructor;
this.acceptHiddenConstructor = acceptHiddenConstructor;
}
protected abstract Map<String,Reader> doFindReaders(Class<?> clazz);
protected abstract Map<String,Writer> doFindWriters(Class<?> clazz);
@Override
public Comparator<String> fieldComparator(final Class<?> clazz) {
return null;
}
@Override
public Map<String, Reader> findReaders(final Class<?> clazz) {
return sanitize(clazz, doFindReaders(clazz));
}
@Override
public Map<String, Writer> findWriters(final Class<?> clazz) {
return sanitize(clazz, doFindWriters(clazz));
}
public void setFieldFilteringStrategy(final FieldFilteringStrategy fieldFilteringStrategy) {
this.fieldFilteringStrategy = fieldFilteringStrategy;
}
public FieldFilteringStrategy getFieldFilteringStrategy() {
return fieldFilteringStrategy;
}
@Override
public ObjectConverter.Reader<?> findReader(final Class<?> clazz) {
return null; // TODO: converter?
}
@Override
public ObjectConverter.Writer<?> findWriter(final Class<?> clazz) {
return null; // TODO: converter?
}
@Override
public Adapter<?, ?> findAdapter(Class<?> clazz) {
return null; // TODO: converter?
}
@Override
public void afterParsed(final Class<?> clazz) {
// no-op
}
@Override
public Factory findFactory(final Class<?> clazz) {
Constructor<?> constructor = null;
if (isRecord(clazz) || Meta.getAnnotation(clazz, JohnzonRecord.class) != null) {
constructor = findRecordConstructor(clazz);
} else {
for (final Constructor<?> c : clazz.getDeclaredConstructors()) {
if (c.getParameterTypes().length == 0) {
if (!Modifier.isPublic(c.getModifiers()) && acceptHiddenConstructor) {
c.setAccessible(true);
}
constructor = c;
if (!useConstructor) {
break;
}
} else if (c.getAnnotation(ConstructorProperties.class) != null) {
constructor = c;
break;
}
}
if (constructor == null) {
try {
constructor = clazz.getConstructor();
} catch (final NoSuchMethodException e) {
return null; // readOnly class
}
}
}
final boolean constructorHasArguments = constructor != null && constructor.getGenericParameterTypes().length > 0;
final Type[] factoryParameterTypes;
final String[] constructorParameters;
final Adapter<?, ?>[] constructorParameterConverters;
final Adapter<?, ?>[] constructorItemParameterConverters;
final ObjectConverter.Codec<?>[] objectConverters;
if (constructorHasArguments) {
factoryParameterTypes = constructor.getGenericParameterTypes();
constructorParameters = new String[constructor.getGenericParameterTypes().length];
final Constructor<?> fc = constructor;
final String[] constructorProperties = ofNullable(constructor.getAnnotation(ConstructorProperties.class))
.map(ConstructorProperties::value)
.orElseGet(() -> Stream.of(fc.getParameters())
.map(p -> ofNullable(p.getAnnotation(JohnzonRecord.Name.class))
.map(JohnzonRecord.Name::value)
.orElseGet(p::getName))
.toArray(String[]::new));
System.arraycopy(constructorProperties, 0, constructorParameters, 0, constructorParameters.length);
constructorParameterConverters = new Adapter<?, ?>[constructor.getGenericParameterTypes().length];
constructorItemParameterConverters = new Adapter<?, ?>[constructorParameterConverters.length];
objectConverters = new ObjectConverter.Codec[constructorParameterConverters.length];
for (int i = 0; i < constructorParameters.length; i++) {
for (final Annotation a : constructor.getParameterAnnotations()[i]) {
if (a.annotationType() == JohnzonConverter.class) {
try {
MapperConverter mapperConverter = JohnzonConverter.class.cast(a).value().newInstance();
if (mapperConverter instanceof Converter) {
final Adapter<?, ?> converter = new ConverterAdapter((Converter) mapperConverter);
if (matches(constructor.getParameterTypes()[i], converter)) {
constructorParameterConverters[i] = converter;
constructorItemParameterConverters[i] = null;
} else {
constructorParameterConverters[i] = null;
constructorItemParameterConverters[i] = converter;
}
} else {
objectConverters[i] = (ObjectConverter.Codec<?>) mapperConverter;
}
} catch (final Exception e) {
throw new IllegalArgumentException(e);
}
}
}
}
} else {
factoryParameterTypes = NO_PARAMS;
constructorParameters = null;
constructorParameterConverters = null;
constructorItemParameterConverters = null;
objectConverters = null;
}
final Constructor<?> cons = constructor;
if (cons != null && !cons.isAccessible()) {
cons.setAccessible(true);
}
return new Factory() {
@Override
public Object create(final Object[] params) {
if (cons == null) {
throw new IllegalArgumentException(clazz.getName() + " can't be instantiated by Johnzon, this is a write only class");
}
try {
return params == null ? cons.newInstance() : cons.newInstance(params);
} catch (final InstantiationException | IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (final InvocationTargetException e) {
throw new IllegalStateException(e.getCause());
}
}
@Override
public Type[] getParameterTypes() {
return factoryParameterTypes;
}
@Override
public String[] getParameterNames() {
return constructorParameters;
}
@Override
public Adapter<?, ?>[] getParameterConverter() {
return constructorParameterConverters;
}
@Override
public Adapter<?, ?>[] getParameterItemConverter() {
return constructorItemParameterConverters;
}
@Override
public ObjectConverter.Codec<?>[] getObjectConverter() {
return objectConverters;
}
};
}
private Constructor<?> findRecordConstructor(Class<?> clazz) {
return Stream.of(clazz.getConstructors())
.max(comparing(Constructor::getParameterCount))
.map(c -> {
if (!c.isAccessible()) {
c.setAccessible(true);
}
return c;
})
.orElse(null);
}
@Override
public Method findAnyGetter(final Class<?> clazz) {
Method m = null;
for (final Method current : clazz.getMethods()) {
if (current.getAnnotation(JohnzonAny.class) != null) {
if (current.getParameterTypes().length == 0) {
if (!Map.class.isAssignableFrom(current.getReturnType())) {
throw new IllegalArgumentException("@JohnzonAny getters can only return a Map<String, Object>");
}
if (m != null) {
throw new IllegalArgumentException("Ambiguous @JohnzonAny on " + m + " and " + current);
}
m = current;
}
}
}
return m;
}
@Override
public Method findAnySetter(final Class<?> clazz) {
Method m = null;
for (final Method current : clazz.getMethods()) {
if (current.getAnnotation(JohnzonAny.class) != null) {
final Class<?>[] parameterTypes = current.getParameterTypes();
if (parameterTypes.length == 2 && parameterTypes[0] == String.class && parameterTypes[1] == Object.class) {
if (m != null) {
throw new IllegalArgumentException("Ambiguous @JohnzonAny on " + m + " and " + current);
}
m = current;
}
}
}
return m;
}
private <T> Map<String, T> sanitize(final Class<?> type, final Map<String, T> delegate) {
for (final String field : fieldFilteringStrategy.select(type)) {
delegate.remove(field);
}
return delegate;
}
public interface FieldFilteringStrategy {
Collection<String> select(final Class<?> type);
}
public static abstract class ConfiguredFieldFilteringStrategy implements FieldFilteringStrategy {
private final Map<Class<?>, Collection<String>> fieldsToRemove = new LinkedHashMap<>();
public ConfiguredFieldFilteringStrategy() {
// mainly built it in the JVM types == user cant handle them
fieldsToRemove.put(Throwable.class, asList("suppressedExceptions", "cause"));
}
public Map<Class<?>, Collection<String>> getFieldsToRemove() {
return fieldsToRemove;
}
}
public static class SingleEntryFieldFilteringStrategy extends ConfiguredFieldFilteringStrategy {
@Override
public Collection<String> select(final Class<?> type) {
for (final Map.Entry<Class<?>, Collection<String>> entry : getFieldsToRemove().entrySet()) {
if (entry.getKey().isAssignableFrom(type)) {
return entry.getValue();
}
}
return emptySet();
}
}
public static class AllEntriesFieldFilteringStrategy extends ConfiguredFieldFilteringStrategy {
@Override
public Collection<String> select(final Class<?> type) {
return getFieldsToRemove().entrySet().stream()
.filter(entry -> entry.getKey().isAssignableFrom(type))
.flatMap(entry -> entry.getValue().stream())
.collect(toSet());
}
}
}