/*
* Copyright 2016 McDowell
*
* 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 uk.kludje;
import uk.kludje.property.PropertyGetterList;
import uk.kludje.property.PropertyType;
import uk.kludje.property.TypedProperty;
import java.util.Objects;
/**
* <p>Provides a basic meta-method builder for common {@code Object} method implementations.</p>
* <p>Example builds equals, hashCode and toString methods using the "id", "name" and "dateOfBirth" properties:</p>
* <pre><code>
* import uk.kludje.Meta;
* import java.time.LocalDate;
* import static uk.kludje.Meta.meta;
*
* public class PersonPojo {
* private static final Meta<PersonPojo> META = meta(PersonPojo.class)
* .longs(pp -> pp.id)
* .objects(pp -> pp.name, pp -> pp.dateOfBirth);
*
* private final long id;
* private final String name;
* private final LocalDate dateOfBirth;
*
* public PersonPojo(long id, String name, LocalDate dateOfBirth) {
* this.id = id;
* this.name = name;
* this.dateOfBirth = dateOfBirth;
* }
*
* public String getName() {
* return name;
* }
*
* public LocalDate getDateOfBirth() {
* return dateOfBirth;
* }
*
* public boolean equals(Object obj) {
* return META.equals(this, obj);
* }
*
* public int hashCode() {
* return META.hashCode(this);
* }
*
* public String toString() {
* return META.toString(this);
* }
* }
* </code></pre>
* <p>
* <p>Note: arrays are treated as objects by default; see {@link MetaConfig#withShallowArraySupport()}.
* If classes can be equal to subtypes use {@link MetaConfig#withInstanceofEqualsTypeCheck()}.</p>
* <p>
* <p>Instances of this type are immutable and thread safe.</p>
*
* @param <T> the accessed type
* @see PropertyGetterList
*/
public final class Meta<T> extends PropertyGetterList<T, Meta<T>> {
private static final ArrayMapper<TypedProperty> TYPED_PROPERTY_ARRAYS = ArrayMapper.arrayMapper(TypedProperty.class);
private static final ArrayMapper<String> TYPED_STRING_ARRAYS = ArrayMapper.arrayMapper(String.class);
private final MetaConfig config;
private final Class<T> type;
private final TypedProperty[] props;
private final String[] names;
private final MetaConfig.InstanceCheckPolicy instancePolicy;
private final MetaConfig.ObjectEqualsPolicy equalsPolicy;
private final MetaConfig.ObjectHashCodePolicy hashcodePolicy;
private final MetaConfig.ObjectToStringPolicy toStringPolicy;
private Meta(MetaConfig config,
Class<T> type,
TypedProperty[] props,
String[] names) {
this.config = config;
this.type = type;
this.props = props;
this.names = names;
this.instancePolicy = config.getInstanceCheckPolicy();
this.equalsPolicy = config.getObjectEqualsPolicy();
this.hashcodePolicy = config.getObjectHashCodePolicy();
this.toStringPolicy = config.getObjectToStringPolicy();
}
@Override
protected Meta<T> newInstance(Meta<T> old, String name, TypedProperty getter) {
Ensure.that(getter != null, "getter != null");
Ensure.that(name != null, "name != null");
TypedProperty[] newProps = TYPED_PROPERTY_ARRAYS.concat(old.props, getter);
String[] newNames = TYPED_STRING_ARRAYS.concat(old.names, name);
return new Meta<>(old.config, old.type, newProps, newNames);
}
/**
* @param type the class of type T
* @param <T> the type
* @return a new instance with no properties
*/
public static <T> Meta<T> meta(Class<T> type) {
Ensure.that(type != null, "type != null");
return new Meta<>(MetaConfig.defaultConfig(), type, TYPED_PROPERTY_ARRAYS.empty(), TYPED_STRING_ARRAYS.empty());
}
/**
* WARNING: when this method is used a subclass can never be equal to a parent type.
*
* @param <T> the type of class
* @return a new instance
*/
public static <T> Meta<T> meta() {
return new Meta<>(MetaConfig.defaultConfig(), null, TYPED_PROPERTY_ARRAYS.empty(), TYPED_STRING_ARRAYS.empty());
}
/**
* Some aspects of the provided methods are caller-configurable.
* Use this method to alter these aspects of the configuration.
* <p>
* If {@link #meta()} was used to instantiate the instance any changes to {@link MetaConfig.ObjectEqualsPolicy}
* will result in an error. Use {@link #meta(Class)} instead.
*
* @param config the new configuration; must not be null
* @return a new instance with the new configuration
*/
public Meta<T> configure(MetaConfig config) {
Ensure.that(config != null, "config != null");
//noinspection ConstantConditions
if (type == null && (config.getInstanceCheckPolicy() != MetaConfig.defaultConfig().getInstanceCheckPolicy())) {
String message = "Not enough type information to guarantee equals contract. Use Meta.meta(Class) to construct this type instead.";
throw new AssertionError(message);
}
return new Meta<>(config, this.type, this.props, this.names);
}
/**
* {@code any} is equal to {@code t} if it is of type {@code T}
* and all the properties defined by this type are equal.
*
* @param t a non-null instance of type T
* @param any any object, including null
* @return true if the arguments are equal
*/
@SuppressWarnings("unchecked")
public boolean equals(T t, Object any) {
Objects.requireNonNull(t);
if (any == null) {
return false;
}
if (any == t) {
return true;
}
if (!this.instancePolicy.isSameType(type == null ? t.getClass() : type, any)) {
return false;
}
T other = (T) any;
for (TypedProperty p : props) {
PropertyType type = p.type();
switch (type) {
case INT:
uk.kludje.property.IntGetter<T> ig = (uk.kludje.property.IntGetter<T>) p;
if (ig.getInt(t) != ig.getInt(other)) {
return false;
}
break;
case OBJECT:
uk.kludje.property.Getter<T> g = (uk.kludje.property.Getter<T>) p;
Object o1 = g.get(t);
Object o2 = g.get(other);
if (!equalsPolicy.areEqual(o1, o2)) {
return false;
}
break;
case BOOLEAN:
uk.kludje.property.BooleanGetter<T> bg = (uk.kludje.property.BooleanGetter<T>) p;
if (bg.getBoolean(t) != bg.getBoolean(other)) {
return false;
}
break;
case LONG:
uk.kludje.property.LongGetter<T> lg = (uk.kludje.property.LongGetter<T>) p;
if (lg.getLong(t) != lg.getLong(other)) {
return false;
}
break;
case CHAR:
uk.kludje.property.CharGetter<T> cg = (uk.kludje.property.CharGetter<T>) p;
if (cg.getChar(t) != cg.getChar(other)) {
return false;
}
break;
case DOUBLE:
uk.kludje.property.DoubleGetter<T> dg = (uk.kludje.property.DoubleGetter<T>) p;
if (dg.getDouble(t) != dg.getDouble(other)) {
return false;
}
break;
case FLOAT:
uk.kludje.property.FloatGetter<T> fg = (uk.kludje.property.FloatGetter<T>) p;
if (fg.getFloat(t) != fg.getFloat(other)) {
return false;
}
break;
case SHORT:
uk.kludje.property.ShortGetter<T> sg = (uk.kludje.property.ShortGetter<T>) p;
if (sg.getShort(t) != sg.getShort(other)) {
return false;
}
break;
case BYTE:
uk.kludje.property.ByteGetter<T> byg = (uk.kludje.property.ByteGetter<T>) p;
if (byg.getByte(t) != byg.getByte(other)) {
return false;
}
break;
default:
throw new AssertionError("Unsupported PropertyType");
}
}
return true;
}
/**
* Creates a hash of all values defined by the properties provided to this instance.
* The hashing formula is not specified.
*
* @param t the non-null instance to create a hash for
* @return the hash
*/
@SuppressWarnings("unchecked")
public int hashCode(T t) {
Objects.requireNonNull(t);
int result = 0;
int prime = 31;
for (TypedProperty p : props) {
result = result * prime;
PropertyType type = p.type();
switch (type) {
case INT:
uk.kludje.property.IntGetter<T> ig = (uk.kludje.property.IntGetter<T>) p;
result += ig.getInt(t);
break;
case OBJECT:
uk.kludje.property.Getter<T> g = (uk.kludje.property.Getter<T>) p;
result += hashcodePolicy.toHashcode(g.get(t));
break;
case BOOLEAN:
uk.kludje.property.BooleanGetter<T> bg = (uk.kludje.property.BooleanGetter<T>) p;
result += bg.getBoolean(t) ? 1 : 0;
break;
case LONG:
uk.kludje.property.LongGetter<T> lg = (uk.kludje.property.LongGetter<T>) p;
result += Long.hashCode(lg.getLong(t));
break;
case CHAR:
uk.kludje.property.CharGetter<T> cg = (uk.kludje.property.CharGetter<T>) p;
result += cg.getChar(t);
break;
case DOUBLE:
uk.kludje.property.DoubleGetter<T> dg = (uk.kludje.property.DoubleGetter<T>) p;
result += Double.hashCode(dg.getDouble(t));
break;
case FLOAT:
uk.kludje.property.FloatGetter<T> fg = (uk.kludje.property.FloatGetter<T>) p;
result += Float.hashCode(fg.getFloat(t));
break;
case SHORT:
uk.kludje.property.ShortGetter<T> sg = (uk.kludje.property.ShortGetter<T>) p;
result += sg.getShort(t);
break;
case BYTE:
uk.kludje.property.ByteGetter<T> byg = (uk.kludje.property.ByteGetter<T>) p;
result += byg.getByte(t);
break;
default:
throw new AssertionError("Unsupported PropertyType");
}
}
return result;
}
/**
* Creates a string form of the type for debugging purposes.
* The exact form is not specified.
*
* @param t the non-null instance to create a string form of
* @return debug string
*/
@SuppressWarnings("unchecked")
public String toString(T t) {
Class<?> tClass = t.getClass();
StringBuilder buf = new StringBuilder();
buf.append(tClass.getSimpleName());
buf.append(" {");
String delim = "";
for (int i = 0; i < props.length; i++) {
buf.append(delim);
delim = ", ";
TypedProperty p = props[i];
String name = names[i];
if (!"".equals(name)) {
buf.append(name);
buf.append('=');
}
PropertyType type = p.type();
switch (type) {
case INT:
uk.kludje.property.IntGetter<T> ig = (uk.kludje.property.IntGetter<T>) p;
buf.append(ig.getInt(t));
break;
case OBJECT:
uk.kludje.property.Getter<T> g = (uk.kludje.property.Getter<T>) p;
String str = this.toStringPolicy.toString(g.apply(t));
buf.append(str);
break;
case BOOLEAN:
uk.kludje.property.BooleanGetter<T> bg = (uk.kludje.property.BooleanGetter<T>) p;
buf.append(bg.getBoolean(t));
break;
case LONG:
uk.kludje.property.LongGetter<T> lg = (uk.kludje.property.LongGetter<T>) p;
buf.append(lg.getLong(t));
break;
case CHAR:
uk.kludje.property.CharGetter<T> cg = (uk.kludje.property.CharGetter<T>) p;
buf.append(cg.getChar(t));
break;
case DOUBLE:
uk.kludje.property.DoubleGetter<T> dg = (uk.kludje.property.DoubleGetter<T>) p;
buf.append(dg.getDouble(t));
break;
case FLOAT:
uk.kludje.property.FloatGetter<T> fg = (uk.kludje.property.FloatGetter<T>) p;
buf.append(fg.getFloat(t));
break;
case SHORT:
uk.kludje.property.ShortGetter<T> sg = (uk.kludje.property.ShortGetter<T>) p;
buf.append(sg.getShort(t));
break;
case BYTE:
uk.kludje.property.ByteGetter<T> byg = (uk.kludje.property.ByteGetter<T>) p;
buf.append(byg.getByte(t));
break;
default:
throw new AssertionError("Unsupported PropertyType");
}
}
return buf.append('}').toString();
}
/**
* @return the number of properties this instance exposes
*/
public int size() {
return props.length;
}
/**
* @param index the getter to return
* @return an object that can be cast to a getter type
* @see uk.kludje.property.Getter
* @see uk.kludje.property.BooleanGetter
* @see uk.kludje.property.ByteGetter
* @see uk.kludje.property.ShortGetter
* @see uk.kludje.property.IntGetter
* @see uk.kludje.property.LongGetter
* @see uk.kludje.property.FloatGetter
* @see uk.kludje.property.DoubleGetter
* @see uk.kludje.property.CharGetter
* @see PropertyType
*/
public TypedProperty propertyAt(int index) {
return props[index];
}
/**
* @param index the name index
* @return the property name for the given index or the empty string
*/
public String nameAt(int index) {
return names[index];
}
/**
* @deprecated this type will be deleted in favour of the uk.kludje.property version
*/
@Deprecated
@FunctionalInterface
public interface Getter<T> extends uk.kludje.property.Getter<T> {
}
/**
* @deprecated this type will be deleted in favour of the uk.kludje.property version
*/
@Deprecated
@FunctionalInterface
public interface BooleanGetter<T> extends uk.kludje.property.BooleanGetter<T> {
}
/**
* @deprecated this type will be deleted in favour of the uk.kludje.property version
*/
@Deprecated
@FunctionalInterface
public interface CharGetter<T> extends uk.kludje.property.CharGetter<T> {
}
/**
* @deprecated this type will be deleted in favour of the uk.kludje.property version
*/
@Deprecated
@FunctionalInterface
public interface ByteGetter<T> extends uk.kludje.property.ByteGetter<T> {
}
/**
* @deprecated this type will be deleted in favour of the uk.kludje.property version
*/
@Deprecated
@FunctionalInterface
public interface ShortGetter<T> extends uk.kludje.property.ShortGetter<T> {
}
/**
* @deprecated this type will be deleted in favour of the uk.kludje.property version
*/
@Deprecated
@FunctionalInterface
public interface IntGetter<T> extends uk.kludje.property.IntGetter<T> {
}
/**
* @deprecated this type will be deleted in favour of the uk.kludje.property version
*/
@Deprecated
@FunctionalInterface
public interface LongGetter<T> extends uk.kludje.property.LongGetter<T> {
}
/**
* @deprecated this type will be deleted in favour of the uk.kludje.property version
*/
@Deprecated
@FunctionalInterface
public interface FloatGetter<T> extends uk.kludje.property.FloatGetter<T> {
}
/**
* @deprecated this type will be deleted in favour of the uk.kludje.property version
*/
@Deprecated
@FunctionalInterface
public interface DoubleGetter<T> extends uk.kludje.property.DoubleGetter<T> {
}
}