All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.impl.ProxyCreator Maven / Gradle / Ivy

There is a newer version: 3.6.1
Show newest version
/*******************************************************************
 * © 2019 SAP SE or an SAP affiliate company. All rights reserved. *
 *******************************************************************/
package com.sap.cds.impl;

import static java.util.Arrays.stream;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiConsumer;
import java.util.function.Function;

import com.google.common.collect.ImmutableMap;
import com.sap.cds.CdsException;
import com.sap.cds.JSONizable;
import com.sap.cds.Row;
import com.sap.cds.Struct.ProxyFactory;
import com.sap.cds.impl.builder.model.StructuredTypeProxy;
import com.sap.cds.impl.parser.token.Jsonizer;
import com.sap.cds.ql.CdsName;
import com.sap.cds.ql.StructuredType;

public class ProxyCreator implements ProxyFactory {

	private static final ConcurrentMap GENERIC_RETURN_TYPE = new ConcurrentHashMap<>();

	private static final ConcurrentMap, ImmutableMap> CLASS_MAPPING_RETURN_TYPE = new ConcurrentHashMap<>();

	private static final Method JSONIZABLE_TO_JSON;
	private static final Method EQUALS;

	static {
		try {
			JSONIZABLE_TO_JSON = JSONizable.class.getDeclaredMethod("toJson");
			EQUALS = Object.class.getDeclaredMethod("equals", Object.class);
		} catch (NoSuchMethodException | SecurityException e) {
			throw new AssertionError("JSONizable::toJson must exist", e); // NOSONAR
		}
	}

	private static  V createIfAbsentPreferGet(ConcurrentMap map, K key,
			Function provider) {
		V res = map.get(key); // do not use computeIfAbsent because ConcurrentHashMap may lock.
		if (res == null) {
			V r = provider.apply(key);
			res = map.putIfAbsent(key, r);
			if (res == null) {
				res = r;
			}
		}
		return res;
	}

	private static Type getGenericReturnType(Method method) {
		return createIfAbsentPreferGet(GENERIC_RETURN_TYPE, method, (m) -> m.getGenericReturnType());
	}

	@Override
	public  T proxy(Map data, Class proxyInterface) {
		return createProxy(data, proxyInterface);
	}

	static  T createProxy(Map data, Class proxyInterface) {
		return createProxy(data, proxyInterface,
				createIfAbsentPreferGet(CLASS_MAPPING_RETURN_TYPE, proxyInterface, ProxyCreator::createMapping));
	}

	@SuppressWarnings("unchecked")
	static  T createProxy(Map data, Class proxyInterface, Map mapping) {
		Row row = data instanceof Row ? (Row) data : RowImpl.row(data);
		return (T) Proxy.newProxyInstance(MapProxy.class.getClassLoader(), new Class[] { proxyInterface },
				new MapProxy(row, mapping));
	}

	static ImmutableMap createMapping(Class type) {
		ImmutableMap.Builder delegates = stream(type.getDeclaredMethods()).collect(
				ImmutableMap::builder,
				(BiConsumer, ? super Method>) (builder, method) -> {
					Delegate delegate = delegate(method);
					if (delegate != null) {
						builder.put(method, delegate);
					}
				}, (b1, b2) -> b1.putAll(b2.build()));
		registerToJson(type, delegates);
		registerEquals(delegates);

		return delegates.build();
	}

	private static void registerToJson(Class type, ImmutableMap.Builder delegates) {
		if (JSONizable.class.isAssignableFrom(type)) {
			delegates.put(JSONIZABLE_TO_JSON, (data, a, p) -> Jsonizer.json(data));
		}
	}

	private static void registerEquals(ImmutableMap.Builder delegates) {
		delegates.put(EQUALS, (data, a, p) -> {
			Object other = a[0];
			if (p == other) {
				return true;
			}
			return data.equals(other);
		});
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	private static Delegate delegate(Method method) {
		CdsName nameAnnotation = method.getAnnotation(CdsName.class);
		String cdsName = nameAnnotation != null ? nameAnnotation.value() : propertyName(method);
		if ("ref".equals(method.getName()) && method.getParameterCount() == 0) {
			return (row, a, p) -> {
				Class type = (Class) method.getReturnType();
				return StructuredTypeProxy.create(row.ref(), type);
			};
		}
		if (isGetter(method)) {
			Class type = method.getReturnType();
			if (Map.class.isAssignableFrom(type) && Map.class != type) {
				return proxyMap(cdsName, type);
			}
			if (Collection.class.isAssignableFrom(type)) {
				return proxyList(method, cdsName);
			}
			return (data, a, p) -> data.get(cdsName);
		}
		if (isSetter(method)) {
			return (data, args, proxy) -> {
				data.put(cdsName, args[0]);
				return proxy;
			};
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	private static Delegate proxyMap(String cdsName, Class type) {
		return (data, a, p) -> {
			Object value = data.get(cdsName);
			if (value == null || value instanceof Proxy) {
				return value;
			}
			return createProxy((Map) value, type);
		};
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	private static Delegate proxyList(Method method, String cdsName) {
		Type returnType = getGenericReturnType(method);
		if (returnType instanceof ParameterizedType) {
			Type type = ((ParameterizedType) returnType).getActualTypeArguments()[0];
			if (type instanceof Class && Map.class.isAssignableFrom((Class) type)) {
				return (data, a, p) -> {
					Object list = data.get(cdsName);
					return list == null ? null : new ProxyList((List>) list, (Class) type);
				};
			} else {
				return (data, a, p) -> data.get(cdsName);
			}
		}
		throw new CdsException(
				"Return type of accessor interface method " + method.getName() + " must be parameterized");
	}

	private static String propertyName(Method method) {
		String key = method.getName();
		if (key.startsWith("set") || key.startsWith("get")) {
			key = lowercaseFromThird(key);
		}
		return key;
	}

	private static String lowercaseFromThird(String s) {
		char[] c = s.toCharArray();
		c[3] = Character.toLowerCase(c[3]);
		return new String(c, 3, s.length() - 3);
	}

	private static boolean isGetter(Method method) {
		return method.getParameterCount() == 0 && !method.getReturnType().equals(Void.TYPE);
	}

	private static boolean isSetter(Method method) {
		return method.getParameterCount() == 1 && (method.getReturnType().equals(Void.TYPE)
				|| method.getReturnType().equals(method.getDeclaringClass()));
	}

	@FunctionalInterface
	static interface Delegate {
		Object invoke(Row row, Object[] args, Object proxy);
	}

	private static class MapProxy implements InvocationHandler {

		private final Row row;
		private final Map delegates;

		protected MapProxy(Row row, Map delegates) {
			this.row = row;
			this.delegates = delegates;
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			Delegate delegate = delegates.get(method);
			if (delegate != null) {
				return delegate.invoke(row, args, proxy);
			}

			try {
				return method.invoke(row, args);
			} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
				throw new CdsException("Failed to invoke method " + method.getName(), e);
			}
		}
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy