com.github.haixing_hu.data.bean.Property.java Source code

Java tutorial

Introduction

Here is the source code for com.github.haixing_hu.data.bean.Property.java

Source

/*
 * Copyright (c) 2014  Haixing Hu
 *
 * 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 com.github.haixing_hu.data.bean;

import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;

import static com.github.haixing_hu.lang.Argument.requireNonEmpty;
import static com.github.haixing_hu.lang.Argument.requireNonNull;

/**
 * A {@link Property} object represents the descriptor of a property of a bean.
 * <p>
 * There are three kinds of properties supported by this library:
 * <ul>
 * <li><b>Simple</b> - Simple, or scalar, properties have a single value that
 * may be retrieved or modified. The underlying property type might be a Java
 * language primitive (such as {@code int}, a simple object (such as a
 * {@code String}), or a more complex object whose class is defined either by
 * the Java language, by the application, or by a class library included with
 * the application.</li>
 * <li><b>Indexed</b> - An indexed property stores an ordered collection of
 * objects (all of the same type) that can be individually accessed by an
 * integer-valued, non-negative index (or subscript). Alternatively, the entire
 * set of values may be set or retrieved using an array. As an extension to the
 * JavaBeans specification, the BeanUtils package considers any property whose
 * underlying data type is java.util.List (or an implementation of List) to be
 * indexed as well.</li>
 * <li><b>Mapped</b> - As an extension to standard JavaBeans APIs, this library
 * considers any property whose underlying value is a {@link java.util.Map} to
 * be "mapped". You can set and retrieve individual values via a {@link String}
 * valued key.</li>
 * </ul>
 *
 * @author Haixing Hu
 */
public final class Property implements Serializable {

    private static final long serialVersionUID = 3888156978033183567L;

    /**
     * The enumeration of types of properties.
     *
     * @author Haixing Hu
     */
    enum Kind {

        /**
         * Represents a simple property.
         */
        SIMPLE,

        /**
         * Represents an indexed property.
         */
        INDEXED,

        /**
         * Represents a mapped property.
         */
        MAPPED,
    }

    private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z_][a-zA-Z_0-9-]*");

    /**
     * Tests whether a name is a valid property name.
     * <p>
     * A valid property name should starts with an ASCII alphabetic character or
     * an underscore, and followed by zero or more alphabetic characters, digital
     * characters, underscores, or hyphens. Formally, its syntax is as follows:
     *
     * <pre>
     * <code>
     * name ::= [a-zA-Z_][a-zA-Z_0-9-]*
     * </code>
     * </pre>
     *
     * @param name
     *          the name to be test.
     * @return {@code true} if the name is a valid property name; {@code false}
     *         otherwise.
     */
    public static boolean isValidName(final String name) {
        if ((name == null) || (name.length() == 0)) {
            return false;
        }
        if (NAME_PATTERN.matcher(name).matches()) {
            return true;
        } else {
            return false;
        }
    }

    private final String name;
    private final Kind kind;
    private final Class<?> type;
    private final Class<?> contentType;

    /**
     * Constructs a {@link Property}.
     * <p>
     * The constructor will determine the kind of the new property according to
     * the value type. If the value type is the class of an Java build-in array,
     * or a sub-class of the {@link java.util.List}, the new property will be an
     * indexed property; if the value type is the sub-class of
     * {@link java.util.Map}, the new property is a mapped property; otherwise,
     * the new property is a simple property.
     *
     * @param name
     *          the name of the property, which cannot be {@code null} nor empty.
     * @param type
     *          the type of the values stored in the property, which cannot be
     *          {@code null}.
     * @throws NullPointerException
     *           if {@code name} or {@code type} is {@code null}; or if the new
     *           property is not simple.
     * @throws IllegalArgumentException
     *           if {@code name} is not a valid property name.
     * @see #isValidName(String)
     */
    public Property(final String name, final Class<?> type) {
        this(name, type, null);
    }

    /**
     * Constructs a {@link Property}.
     * <p>
     * The constructor will determine the kind of the new property according to
     * the value type. If the value type is the class of an Java build-in array,
     * or a sub-class of the {@link java.util.List}, the new property will be an
     * indexed property; if the value type is the sub-class of
     * {@link java.util.Map}, the new property is a mapped property; otherwise,
     * the new property is a simple property.
     *
     * @param name
     *          the name of the property, which cannot be {@code null} and must be
     *          a valid property name.
     * @param type
     *          the type of the values stored in the property, which cannot be
     *          {@code null}.
     * @param contentType
     *          the type of contents stored in the value of this property; or
     *          {@code null} if this property is a simple property. But if the
     *          property is not a simple property, this argument must be provided
     *          and cannot be {@code null}.
     * @throws NullPointerException
     *           if {@code name} or {@code type} is {@code null}; or if the new
     *           property is not simple, but {@code contentType} is {@code null}.
     * @throws IllegalArgumentException
     *           if {@code name} is not a valid property name.
     * @see #isValidName(String)
     */
    public Property(final String name, final Class<?> type, @Nullable final Class<?> contentType) {
        this.name = requireNonEmpty("name", name);
        this.type = requireNonNull("type", type);
        this.contentType = contentType;
        if (!isValidName(name)) {
            throw new IllegalArgumentException("Invalid property name: " + name);
        }
        if (Map.class.isAssignableFrom(type)) {
            kind = Kind.MAPPED;
        } else if (List.class.isAssignableFrom(type)) {
            kind = Kind.INDEXED;
        } else if (type.isArray()) {
            kind = Kind.INDEXED;
        } else {
            kind = Kind.SIMPLE;
        }
        if ((kind != Kind.SIMPLE) && (contentType == null)) {
            throw new NullPointerException("The contentType cannot be null for non-simple property.");
        }
    }

    /**
     * Gets the name of this property.
     *
     * @return the name of this property.
     */
    public String getName() {
        return name;
    }

    /**
     * Tests whether this property is a simple property.
     *
     * @return {@code true} if this property is a simple property; {@code false}
     *         otherwise.
     */
    public boolean isSimple() {
        return kind == Kind.SIMPLE;
    }

    /**
     * Tests whether this property is an indexed property.
     *
     * @return {@code true} if this property is an indexed property; {@code false}
     *         otherwise.
     */
    public boolean isIndexed() {
        return kind == Kind.INDEXED;
    }

    /**
     * Tests whether this property is a mapped property.
     *
     * @return {@code true} if this property is a mapped property; {@code false}
     *         otherwise.
     */
    public boolean isMapped() {
        return kind == Kind.MAPPED;
    }

    /**
     * Gets the kind of this property.
     *
     * @return the kind of this property.
     */
    public Kind getKind() {
        return kind;
    }

    /**
     * Gets the type of values stored in this property.
     *
     * @return the type of values stored in this property.
     */
    public Class<?> getType() {
        return type;
    }

    /**
     * Gets the type of contents stored in the value of this property.
     * <p>
     * The content type is only meaningful for indexed or mapped properties. The
     * simple property has no content type, therefore calling this function on a
     * simple property will returns {@code null}.
     *
     * @return the type of contents stored in the value of this property; or
     *         {@code null} if this property is a simple property.
     */
    public Class<?> getContentType() {
        return contentType;
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(11, 13).append(name).append(kind).append(type).append(contentType).build();
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj == this) {
            return true;
        }
        if (obj.getClass() != getClass()) {
            return false;
        }
        final Property rhs = (Property) obj;
        return new EqualsBuilder().append(name, rhs.name).append(kind, rhs.kind).append(type, rhs.type)
                .append(contentType, rhs.contentType).build();
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this).append("name", name).append("kind", kind).append("type", type)
                .append("contentType", contentType).build();
    }
}