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

io.activej.serializer.def.impl.ClassSerializerDef Maven / Gradle / Ivy

Go to download

Extremely fast and space-efficient serializers. Implemented using bytecode engineering.

There is a newer version: 6.0-rc2
Show newest version
/*
 * Copyright (C) 2020 ActiveJ LLC.
 *
 * 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.
 */

package io.activej.serializer.def.impl;

import io.activej.codegen.expression.Expression;
import io.activej.codegen.expression.Expressions;
import io.activej.codegen.expression.Variable;
import io.activej.common.annotation.ExposedInternals;
import io.activej.common.builder.AbstractBuilder;
import io.activej.serializer.CompatibilityLevel;
import io.activej.serializer.def.AbstractSerializerDef;
import io.activej.serializer.def.SerializerDef;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.Type;

import java.lang.reflect.*;
import java.util.*;
import java.util.function.UnaryOperator;

import static io.activej.codegen.expression.Expressions.*;
import static io.activej.common.Checks.checkArgument;
import static io.activej.common.Checks.checkNotNull;
import static io.activej.common.Utils.toHashMap;
import static java.lang.Character.toUpperCase;
import static java.lang.String.format;
import static java.lang.reflect.Modifier.*;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toSet;
import static org.objectweb.asm.Type.*;

@ExposedInternals
public final class ClassSerializerDef extends AbstractSerializerDef {

	public final Class encodeType;
	public final Class decodeType;

	public record PropertyDef(Member member, SerializerDef serializer, String name, int versionAdded, int versionDeleted, Object defaultValue) {
		public PropertyDef {
			checkArgument(member instanceof Method || member instanceof Field, "Property %s must be either Method or Field", member);
			checkNotNull(serializer);
			checkNotNull(name);
			checkArgument(versionAdded >= -1);
			checkArgument(versionDeleted >= -1);
		}

		public PropertyDef(Member member, SerializerDef serializer) {
			this(member, serializer, member instanceof Method method ? stripGet(method.getName(), method.getReturnType()) : ((Field) member).getName());
		}

		public PropertyDef(Member member, SerializerDef serializer, String name) {
			this(member, serializer, name, -1, -1, null);
		}

		public boolean hasVersion(int version) {
			if (versionAdded == -1 && versionDeleted == -1) {
				return true;
			}
			if (versionAdded != -1 && versionDeleted == -1) {
				return version >= versionAdded;
			}
			if (versionAdded == -1) {
				return version < versionDeleted;
			}
			if (versionAdded > versionDeleted) {
				return version < versionDeleted || version >= versionAdded;
			}
			if (versionAdded < versionDeleted) {
				return version >= versionAdded && version < versionDeleted;
			}
			throw new IllegalStateException("Added and deleted versions are equal");
		}

		public Class getRawType() {
			if (member instanceof Field field)
				return field.getType();
			if (member instanceof Method method)
				return method.getReturnType();
			throw new AssertionError();
		}

		public Expression defaultExpression() {
			if (defaultValue != null) return Expressions.value(defaultValue, getRawType());
			Type asmType = getType(getRawType());
			return switch (asmType.getSort()) {
				case BOOLEAN -> value(false);
				case CHAR -> value((char) 0);
				case BYTE -> value((byte) 0);
				case SHORT -> value((short) 0);
				case INT -> value(0);
				case Type.LONG -> value(0L);
				case Type.FLOAT -> value(0f);
				case Type.DOUBLE -> value(0d);
				case ARRAY, OBJECT -> nullRef(asmType);
				default -> throw new IllegalArgumentException("Unsupported type " + asmType);
			};
		}
	}

	public final List properties;

	public record SetterDef(Method method, List properties) {}

	public final List setters;

	public record FactoryDef(Executable member, List properties) {}

	public @Nullable FactoryDef factory;

	public ClassSerializerDef(
		Class encodeType, Class decodeType,
		List properties, List setters, @Nullable FactoryDef factory
	) {
		var propertyNames = properties.stream().map(PropertyDef::name).collect(toSet());
		checkArgument(setters.stream().allMatch(setter -> propertyNames.containsAll(setter.properties)));
		checkArgument(factory == null || propertyNames.containsAll(factory.properties));
		this.encodeType = encodeType;
		this.decodeType = decodeType;
		this.properties = properties;
		this.setters = setters;
		this.factory = factory;
	}

	public static Builder builder(Class type) {
		return builder(type, type);
	}

	public static Builder builder(Class encodeType, Class decodeType) {
		checkArgument(!decodeType.isInterface(), "Cannot serialize an interface " + decodeType.getName());
		checkArgument(encodeType.isAssignableFrom(decodeType), format("Class %s should be assignable from %s", encodeType, decodeType));

		return new ClassSerializerDef(encodeType, decodeType, new ArrayList<>(), new ArrayList<>(), null).new Builder();
	}

	public final class Builder extends AbstractBuilder {
		private Builder() {}

		public Builder withField(Field field, SerializerDef serializer, int added, int removed) {
			checkNotBuilt(this);
			checkArgument(isPublic(field.getModifiers()), "Field %s should be public", field);
			ClassSerializerDef.this.properties.removeIf(property ->
				property.member instanceof Field f &&
				Objects.equals(f.getName(), field.getName()));
			String name = field.getName();
			PropertyDef propertyDef = new PropertyDef(field, serializer, name, added, removed, null);
			return withPropertyDef(propertyDef);
		}

		public Builder withGetter(Method method, SerializerDef serializer, int added, int removed) {
			checkNotBuilt(this);
			checkArgument(method.getGenericParameterTypes().length == 0, "Method %s should have 0 generic parameter types", method);
			checkArgument(isPublic(method.getModifiers()), "Method %s should be public", method);
			ClassSerializerDef.this.properties.removeIf(property ->
				property.member instanceof Method m &&
				Objects.equals(m.getName(), method.getName()) &&
				Arrays.equals(m.getParameterTypes(), method.getParameterTypes()));
			String name = stripGet(method.getName(), method.getReturnType());
			return withPropertyDef(new PropertyDef(method, serializer, name, added, removed, null));
		}

		public Builder withPropertyDef(PropertyDef propertyDef) {
			properties.add(propertyDef);
			return this;
		}

		public Builder withSetter(Method method, List properties) {
			checkNotBuilt(this);
			checkArgument(!isPrivate(method.getModifiers()), format("Setter cannot be private: %s", method));
			checkArgument(method.getGenericParameterTypes().length == properties.size(),
				"Number of arguments of a method %s should match a size of list of properties (%d)", method, properties.size());
			ClassSerializerDef.this.setters.removeIf(setter ->
				Objects.equals(setter.method.getName(), method.getName()) &&
				Arrays.equals(setter.method.getParameterTypes(), method.getParameterTypes()));
			return withSetterDef(new SetterDef(method, properties));
		}

		public Builder withSetterDef(SetterDef setterDef) {
			ClassSerializerDef.this.setters.add(setterDef);
			return this;
		}

		public Builder withConstructor(Constructor constructor, List properties) {
			checkNotBuilt(this);
			checkArgument(!isPrivate(constructor.getModifiers()), format("Constructor cannot be private: %s", constructor));
			checkArgument(constructor.getGenericParameterTypes().length == properties.size(),
				"Number of arguments of a constructor %s should match a size of list of properties (%d)", constructor, properties.size());
			return withFactoryDef(new FactoryDef(constructor, properties));
		}

		public Builder withStaticFactoryMethod(Method staticFactoryMethod, List properties) {
			checkNotBuilt(this);
			checkArgument(!isPrivate(staticFactoryMethod.getModifiers()), format("Factory cannot be private: %s", staticFactoryMethod));
			checkArgument(isStatic(staticFactoryMethod.getModifiers()), format("Factory must be static: %s", staticFactoryMethod));
			checkArgument(staticFactoryMethod.getGenericParameterTypes().length == properties.size(),
				"Number of arguments of a method %s should match a size of list of properties (%d)", staticFactoryMethod, properties.size());
			return withFactoryDef(new FactoryDef(staticFactoryMethod, properties));
		}

		public Builder withFactoryDef(FactoryDef factoryDef) {
			ClassSerializerDef.this.factory = factoryDef;
			return this;
		}

		public Builder withMatchingSetters() {
			checkNotBuilt(this);
			Set usedProperties = new HashSet<>();
			if (factory != null) {
				usedProperties.addAll(factory.properties);
			}
			for (var setter : setters) {
				usedProperties.addAll(setter.properties);
			}
			for (var property : properties) {
				if (!(property.member instanceof Method getter)) continue;
				if (usedProperties.contains(property.name)) continue;
				String setterName = "set" + toUpperCase(property.name.charAt(0)) + property.name.substring(1);
				try {
					Method setter = decodeType.getMethod(setterName, getter.getReturnType());
					if (!isPrivate(setter.getModifiers())) {
						withSetter(setter, List.of(property.name));
					}
				} catch (NoSuchMethodException e) {
					throw new IllegalArgumentException(e);
				}
			}
			return this;
		}

		@Override
		protected ClassSerializerDef doBuild() {
			return ClassSerializerDef.this;
		}
	}

	@Override
	public void accept(Visitor visitor) {
		for (var property : properties) {
			visitor.visit(property.name, property.serializer);
		}
	}

	@Override
	public Set getVersions() {
		Set versions = new HashSet<>();
		for (var property : properties) {
			if (property.versionAdded != -1) {
				versions.add(property.versionAdded);
			}
			if (property.versionDeleted != -1) {
				versions.add(property.versionDeleted);
			}
		}
		return versions;
	}

	@Override
	public boolean isInline(int version, CompatibilityLevel compatibilityLevel) {
		return false;// fields.size() <= 1;
	}

	@Override
	public Class getEncodeType() {
		return encodeType;
	}

	@Override
	public Class getDecodeType() {
		return decodeType;
	}

	@Override
	public Expression encode(StaticEncoders staticEncoders, Expression buf, Variable pos, Expression value, int version, CompatibilityLevel compatibilityLevel) {
		List list = new ArrayList<>();
		for (var property : properties) {
			if (!property.hasVersion(version)) continue;
			Encoder encoder = property.serializer.defineEncoder(staticEncoders, version, compatibilityLevel);
			Expression expression;
			Class fieldType = property.serializer.getEncodeType();
			if (property.member instanceof Field field) {
				expression = encoder.encode(buf, pos, cast(property(value, field.getName()), fieldType));
			} else if (property.member instanceof Method method) {
				expression = encoder.encode(buf, pos, cast(call(value, method.getName()), fieldType));
			} else {
				throw new AssertionError();
			}
			list.add(expression);
		}
		return sequence(list);
	}

	@Override
	public Expression decode(StaticDecoders staticDecoders, Expression in, int version, CompatibilityLevel compatibilityLevel) {
		return decoder(staticDecoders, in, version, compatibilityLevel, value -> sequence());
	}

	public Expression decoder(StaticDecoders staticDecoders, Expression in, int version, CompatibilityLevel compatibilityLevel, UnaryOperator instanceInitializer) {
		Map propertyMap = properties.stream().collect(toHashMap(p -> p.name, identity()));

		return let(
			propertyExpressions -> {
				for (PropertyDef propertyDef : properties) {
					if (!propertyDef.hasVersion(version)) continue;
					propertyExpressions.add(
						propertyDef.serializer.defineDecoder(staticDecoders, version, compatibilityLevel).decode(in));
				}
			},
			propertyValues -> {

				Map propertyValuesMap = new HashMap<>();
				int i = 0;
				for (var propertyDef : properties) {
					if (!propertyDef.hasVersion(version)) continue;
					propertyValuesMap.put(propertyDef.name, propertyValues[i++]);
				}

				return let(
					factory == null ?
						constructor(decodeType) :
						factory.member instanceof Constructor ?
							constructor(decodeType,
								extractParameters(factory.member, factory.properties, version, propertyMap, propertyValuesMap)) :
							staticCall(factory.member.getDeclaringClass(), factory.member.getName(),
								extractParameters(factory.member, factory.properties, version, propertyMap, propertyValuesMap)),
					instance -> sequence(seq -> {
						seq.add(instanceInitializer.apply(instance));
						for (var setter : setters) {
							if (setter.properties.stream().map(propertyMap::get).noneMatch(propertyDef -> propertyDef.hasVersion(version))) {
								continue;
							}
							seq.add(
								call(instance, setter.method.getName(),
									extractParameters(setter.method, setter.properties, version, propertyMap, propertyValuesMap)));
						}

						for (var propertyDef : properties) {
							if (!propertyDef.hasVersion(version)) continue;
							if (!(propertyDef.member instanceof Field field)) continue;
							if (isFinal(field.getModifiers())) continue;
							seq.add(
								set(property(instance, field.getName()),
									cast(propertyValuesMap.get(propertyDef.name), propertyDef.getRawType())));
						}

						return instance;
					}));

			});
	}

	private static Expression[] extractParameters(
		Executable executable, List properties, int version,
		Map propertyMap, Map propertyValuesMap
	) {
		Expression[] parameters = new Expression[properties.size()];
		for (int i = 0; i < properties.size(); i++) {
			String property = properties.get(i);
			PropertyDef propertyDef = propertyMap.get(property);
			if (propertyDef == null)
				throw new NullPointerException(format("Field '%s' is not found in '%s'", property, executable));
			if (propertyDef.hasVersion(version)) {
				parameters[i] = cast(propertyValuesMap.get(property), executable.getParameterTypes()[i]);
			} else {
				parameters[i] = cast(propertyDef.defaultExpression(), executable.getParameterTypes()[i]);
			}
		}
		return parameters;
	}

	private static String stripGet(String getterName, Class type) {
		if (type == Boolean.TYPE || type == BooleanSerializerDef.class) {
			if (getterName.startsWith("is") && getterName.length() > 2) {
				return Character.toLowerCase(getterName.charAt(2)) + getterName.substring(3);
			}
		}
		if (getterName.startsWith("get") && getterName.length() > 3) {
			return Character.toLowerCase(getterName.charAt(3)) + getterName.substring(4);
		}
		return getterName;
	}

	@Override
	public String toString() {
		return "ClassSerializerDef{" + encodeType.getSimpleName() + '}';
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy