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

org.rapidoid.beany.Beany Maven / Gradle / Ivy

The newest version!
/*-
 * #%L
 * rapidoid-commons
 * %%
 * Copyright (C) 2014 - 2018 Nikolche Mihajlovski and contributors
 * %%
 * Licensed 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.
 * #L%
 */

package org.rapidoid.beany;

import org.rapidoid.RapidoidThing;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.annotation.ToString;
import org.rapidoid.annotation.Transient;
import org.rapidoid.cls.Cls;
import org.rapidoid.cls.TypeKind;
import org.rapidoid.collection.Coll;
import org.rapidoid.commons.Dates;
import org.rapidoid.lambda.Mapper;
import org.rapidoid.log.Log;
import org.rapidoid.u.U;
import org.rapidoid.var.Var;
import org.rapidoid.var.Vars;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.Map.Entry;


@Authors("Nikolche Mihajlovski")
@Since("2.0.0")
public class Beany extends RapidoidThing {

	private static final String GETTER = "^(get|is)[A-Z].*";

	private static final String SETTER = "^set[A-Z].*";

	protected static final Map, BeanProperties> BEAN_PROPERTIES = Coll
		.autoExpandingMap(new Mapper, BeanProperties>() {

			@Override
			public BeanProperties map(Class clazz) throws Exception {
				Map properties = new LinkedHashMap();

				getBeanProperties(clazz, properties);

				for (Entry e : properties.entrySet()) {
					e.getValue().init();
				}

				return new BeanProperties(properties);
			}
		});

	private static void getBeanProperties(Class clazz, Map properties) {

		if (clazz == null || clazz == Object.class) {
			return;
		} else {
			getBeanProperties(clazz.getSuperclass(), properties);
			for (Class interf : clazz.getInterfaces()) {
				getBeanProperties(interf, properties);
			}
		}

		if (Proxy.isProxyClass(clazz)) {
			return;
		}

		try {
			Method[] methods = clazz.getDeclaredMethods();
			for (Method method : methods) {
				int modif = method.getModifiers();
				Class[] params = method.getParameterTypes();
				Class ret = method.getReturnType();

				String name = method.getName();

				if (!name.startsWith("_") && !name.contains("$")
					&& !Modifier.isPrivate(modif) && !Modifier.isProtected(modif) && !Modifier.isStatic(modif)
					&& !method.isAnnotationPresent(Transient.class)) {

					if ((name.matches(GETTER) && params.length == 0) || (name.matches(SETTER) && params.length == 1)) {

						String propName;
						if (name.startsWith("is")) {
							propName = name.substring(2, 3).toLowerCase() + name.substring(3);
						} else {
							propName = name.substring(3, 4).toLowerCase() + name.substring(4);
						}

						BeanProp prop = properties.get(propName);

						if (prop == null) {
							prop = new BeanProp(propName);
							properties.put(propName, prop);
						}

						if (name.startsWith("set")) {
							prop.setSetter(method);
							prop.setReadOnly(false);
						} else {
							prop.setGetter(method);
						}
					} else if (!name.matches("^to[A-Z].*") && !name.equals("hashCode")) {

						if (params.length == 0 && !ret.equals(void.class)) {

							String propName = name;
							BeanProp prop = properties.get(propName);

							if (prop == null) {
								prop = new BeanProp(propName);
								properties.put(propName, prop);
							}

							prop.setGetter(method);

						} else if (params.length == 1) {

							String propName = name;
							BeanProp prop = properties.get(propName);

							if (prop == null) {
								prop = new BeanProp(propName);
								properties.put(propName, prop);
							}

							prop.setReadOnly(false);
							prop.setSetter(method);
						}
					}
				}
			}

			// remove properties with setters, without getters
			for (Iterator> it = properties.entrySet().iterator(); it.hasNext(); ) {
				Entry entry = it.next();
				BeanProp minfo = entry.getValue();

				if (minfo.getGetter() == null && minfo.getSetter() != null) {
					it.remove();
				}
			}

			Field[] fields = clazz.getDeclaredFields();
			for (Field field : fields) {

				int modif = field.getModifiers();
				if (!Modifier.isPrivate(modif) && !Modifier.isProtected(modif) && !Modifier.isStatic(modif)) {

					String fieldName = field.getName();

					if (!fieldName.startsWith("_") && !fieldName.contains("$")
						&& !field.isAnnotationPresent(Transient.class)) {

						BeanProp prop = properties.get(fieldName);

						if (prop == null) {
							prop = new BeanProp(fieldName, field, (modif & Modifier.FINAL) != 0);
							properties.put(fieldName, prop);
						}
					}
				}
			}

		} catch (Exception e) {
			throw U.rte(e);
		}
	}

	public static void reset() {
		BEAN_PROPERTIES.clear();
	}

	public static BeanProperties propertiesOf(Class clazz) {
		return BEAN_PROPERTIES.get(clazz);
	}

	@SuppressWarnings({"rawtypes", "unchecked"})
	public static BeanProperties propertiesOf(Object obj) {
		if (obj == null) {
			return BeanProperties.NONE;

		} else if (obj instanceof Map) {
			return BeanProperties.from(((Map) obj));

		} else if (obj instanceof Class) {
			return propertiesOf((Class) obj);

		} else {
			return propertiesOf(obj.getClass());
		}
	}

	public static Prop property(Class clazz, String property, boolean mandatory) {
		Prop prop = BEAN_PROPERTIES.get(clazz).get(property);

		if (prop == null && JSProp.is(property)) {
			prop = new JSProp(property);
		}

		if (prop == null && ActionsProp.is(property)) {
			prop = new ActionsProp();
		}

		if (mandatory && prop == null) {
			throw U.rte("Cannot find the property '%s' in the class '%s'", property, clazz);
		}

		return prop;
	}

	public static Object serialize(Object value) {

		if (Cls.kindOf(value) != TypeKind.UNKNOWN || value instanceof Enum) {
			return value;

		} else if (value instanceof Var) {
			Var var = (Var) value;
			return serialize(U.map(var.name(), var.get()));

		} else if (value instanceof Set) {
			Set set = U.set();
			for (Object item : ((Set) value)) {
				set.add(serialize(item));
			}
			return set;

		} else if (value instanceof List) {
			List list = U.list();
			for (Object item : ((List) value)) {
				list.add(serialize(item));
			}
			return list;

		} else if (value instanceof Object[]) {
			Object[] vals = (Object[]) value;
			Object[] arr = new Object[vals.length];
			for (int i = 0; i < arr.length; i++) {
				arr[i] = serialize(vals[i]);
			}
			return arr;

		} else if (value instanceof Map) {
			Map map = U.map();
			for (Entry e : ((Map) value).entrySet()) {
				map.put(serialize(e.getKey()), serialize(e.getValue()));
			}
			return map;

		} else if (value.getClass().isArray()) {
			// a primitive array
			return value;
		} else {
			return read(value);
		}
	}

	public static Map read(Object bean) {
		Map props = U.map();
		read(bean, props);
		return props;
	}

	public static void read(Object bean, Map dest) {
		U.notNull(bean, "bean");
		U.must(!(bean instanceof Collection));
		U.must(!bean.getClass().isArray());
		U.must(!bean.getClass().isEnum());
		U.must(Cls.kindOf(bean) == TypeKind.UNKNOWN);

		for (Prop prop : propertiesOf(bean)) {
			Object value = prop.getRaw(bean);

			if (value instanceof SerializableBean) {
				SerializableBean ser = (SerializableBean) value;
				value = ser.serializeBean();
			} else {
				value = serialize(unwrap(value));
			}

			dest.put(prop.getName(), value);
		}
	}

	public static void bind(Object destBean, Map src) {
		Method bind = Cls.findMethod(destBean.getClass(), "bind", Map.class);

		if (bind != null) {
			Cls.invoke(bind, destBean, src);
		} else {
			update(destBean, src, false, false);
		}
	}

	public static void update(Object destBean, Map src) {
		update(destBean, src, false, false);
	}

	public static void update(Object destBean, Map src, boolean ignoreNullValues) {
		update(destBean, src, ignoreNullValues, false);
	}

	@SuppressWarnings("unchecked")
	public static void update(Object destBean, Map src, boolean ignoreNullValues,
	                          boolean ignoreReadOnlyProperties) {

		if (destBean instanceof Map) {
			((Map) destBean).putAll(src);
			return;
		}

		for (Prop prop : propertiesOf(destBean)) {
			Object value = src.get(prop.getName());

			if (ignoreReadOnlyProperties && prop.isReadOnly()) {
				continue;
			}

			if (value == null) {
				// differentiate non-existing entries from existing entries with null value
				if (ignoreNullValues || !src.containsKey(prop.getName())) {
					continue;
				}
			}

			Object propValue = prop.getRaw(destBean);
			if (propValue != null && propValue instanceof SerializableBean) {
				SerializableBean ser = (SerializableBean) propValue;
				ser.deserializeBean(value);
			} else {
				prop.set(destBean, value);
			}
		}
	}

	@SuppressWarnings("rawtypes")
	public static Prop property(Object obj, String property, boolean mandatory) {
		if (obj instanceof Map) {
			Object value = ((Map) obj).get(property);
			return new MapProp(property, property, Cls.of(value));
		} else {
			return property(obj.getClass(), property, mandatory);
		}
	}

	public static boolean hasProperty(Class clazz, String property) {
		return property(clazz, property, false) != null;
	}

	public static boolean hasProperty(Object obj, String property) {
		return property(obj, property, false) != null;
	}

	@SuppressWarnings("unchecked")
	public static void setPropValue(Object instance, String propertyName, Object value) {
		if (instance instanceof Map) {
			((Map) instance).put(propertyName, value);
		} else {
			property(instance, propertyName, true).set(instance, value);
		}
	}

	@SuppressWarnings("unchecked")
	public static  T getPropValue(Object instance, String propertyName, T defaultValue) {
		if (instance instanceof Map) {
			Map map = (Map) instance;
			return (T) (map.containsKey(propertyName) ? map.get(propertyName) : defaultValue);
		} else {
			Prop prop = property(instance, propertyName, false);
			return prop != null ? (T) prop.get(instance) : defaultValue;
		}
	}

	@SuppressWarnings("unchecked")
	public static  T getPropValue(Object instance, String propertyName) {
		if (instance instanceof Map) {
			Map map = (Map) instance;
			U.must(map.containsKey(propertyName), "The map must contain key: %s", propertyName);
			return (T) map.get(propertyName);
		} else {
			return property(instance.getClass(), propertyName, true).get(instance);
		}
	}

	public static  T getPropValueOfType(Object obj, String property, Class returnType) {
		T value = getPropValueOfType(obj, property, returnType, null);

		if (value == null) {
			throw U.rte("The property '%s' cannot be null!", property);
		}

		return value;
	}

	public static  T getPropValueOfType(Object obj, String property, Class returnType, T defaultValue) {
		Object id = getPropValue(obj, property, null);
		return id != null ? Cls.convert(id, returnType) : defaultValue;
	}

	public static void setId(Object obj, Object id) {
		setPropValue(obj, "id", id);
	}

	public static void setId(Object obj, long id) {
		setId(obj, "" + id);
	}

	public static Object getId(Object obj) {
		return getPropValueOfType(obj, "id", Object.class);
	}

	public static long getLongId(Object obj) {
		return getPropValueOfType(obj, "id", Long.class);
	}

	public static Object getIdIfExists(Object obj) {
		return getPropValueOfType(obj, "id", Object.class, null);
	}

	public static Long getLongIdIfExists(Object obj) {
		return getPropValueOfType(obj, "id", Long.class, null);
	}

	public static Object[] getIds(Object... objs) {
		Object[] ids = new String[objs.length];

		for (int i = 0; i < objs.length; i++) {
			ids[i] = getId(objs[i]);
		}

		return ids;
	}

	public static String beanToStr(Object bean, boolean allowCustom) {

		Class clazz = Cls.unproxy(bean.getClass());

		if (allowCustom) {
			Method m = Cls.getMethod(clazz, "toString");
			if (!m.getDeclaringClass().equals(Object.class)) {
				return bean.toString();
			}
		}

		BeanProperties props = propertiesOf(bean).annotated(ToString.class);

		if (props.isEmpty()) {

			Prop nameProp = property(bean, "name", false);
			if (nameProp != null && nameProp.getType() == String.class) {
				return U.safe((String) nameProp.get(bean));
			}

			props = propertiesOf(bean);
		}

		StringBuilder sb = new StringBuilder();

		for (Prop prop : props) {
			String name = prop.getName();
			Object value = prop.get(bean);

			if (sb.length() > 0) {
				sb.append(", ");
			}

			if (prop.getTypeKind() == TypeKind.UNKNOWN) {
				value = value == bean ? "[this]" : "[obj]";
			}

			if (value instanceof Date) {
				Date date = (Date) value;
				value = Dates.str(date);
			}

			sb.append(name);
			sb.append(": ");
			sb.append(value);
		}

		return sb.toString();
	}

	public static String beanToNiceText(Object bean, boolean allowCustom) {

		Class clazz = Cls.unproxy(bean.getClass());

		if (allowCustom) {
			Method m = Cls.getMethod(clazz, "toString");
			if (!m.getDeclaringClass().equals(Object.class)) {
				return bean.toString();
			}
		}

		BeanProperties props = propertiesOf(bean).annotated(ToString.class);

		if (props.isEmpty()) {

			Prop nameProp = property(bean, "name", false);
			if (nameProp != null && nameProp.getType() == String.class) {
				return U.safe((String) nameProp.get(bean));
			}

			props = propertiesOf(bean);
		}

		StringBuilder sb = new StringBuilder();

		for (Prop prop : props) {
			String name = prop.getName();
			Object value = prop.get(bean);

			if (value != null && prop.getTypeKind() != TypeKind.UNKNOWN && prop.getTypeKind() != TypeKind.DATE) {

				if (sb.length() > 0) {
					sb.append(", ");
				}

				if (!(value instanceof String)) {
					sb.append(name);
					sb.append(": ");
				}

				sb.append(value);
			}
		}

		return sb.toString();
	}

	public static  Comparator comparator(final String orderBy) {
		final int sign = orderBy.startsWith("-") ? -1 : 1;
		final String order = sign == 1 ? orderBy : orderBy.substring(1);

		Comparator comparator = new Comparator() {
			@Override
			public int compare(E o1, E o2) {

				try {
					E val1 = getPropValue(o1, order);
					E val2 = getPropValue(o2, order);

					U.must(val1 == null || val1 instanceof Comparable, "The property '%s' (%s) is not comparable!",
						order, Cls.of((val1)));
					U.must(val2 == null || val2 instanceof Comparable, "The property '%s' (%s) is not comparable!",
						order, Cls.of((val2)));

					return sign * U.compare(val1, val2);
				} catch (Exception e) {
					Log.error("Cannot compare values by: " + orderBy, e);
					return 0;
				}
			}
		};
		return comparator;
	}

	@SuppressWarnings("unchecked")
	public static  List projection(Collection coll, String propertyName) {
		List projection = U.list();

		for (FROM item : coll) {
			projection.add((TO) getPropValue(item, propertyName));
		}

		return projection;
	}

	public static Object unwrap(Object value) {
		return Vars.unwrap(value);
	}

}