org.netflux.core.Record.java Source code

Java tutorial

Introduction

Here is the source code for org.netflux.core.Record.java

Source

/* 
 * netflux-core - Copyright (C) 2005 OPEN input - http://www.openinput.com/
 *
 * This program is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU General Public License as published by the 
 * Free Software Foundation; either version 2 of the License, or (at your 
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT 
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
 * FITNESS FOR A PARTICULAR PURPOSE. 
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along 
 * with this program; if not, write to 
 *   the Free Software Foundation, Inc., 
 *   59 Temple Place, Suite 330, 
 *   Boston, MA 02111-1307 USA
 *   
 * $Id$
 */
package org.netflux.core;

import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.ResourceBundle;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <p>
 * A collection of related items of information ({@link Field}s) treated as a unit. A <code>Record</code> consists of a
 * {@link org.netflux.core.RecordMetadata} describing the data this <code>Record</code> can handle, and a list of {@link Field}s
 * containing the real data.
 * </p>
 * <p>
 * A <code>Record</code> must always be created supplying the corresponding metadata, describing the data this record may handle.
 * Once created you may think of a record as an ordered list of holes where you can place the data allowed by the corresponding field
 * metadata. These holes are initially empty (<code>null</code>), indicating that no piece of data has yet been placed in the
 * record. You may place a {@link Field} instance in any of these holes, even a field with a <code>null</code> value. In this case,
 * you can differentiate a unassigned hole and an assigned hole with a <code>null</code> value.
 * </p>
 * <p>
 * In order to provide a way to signal the end of data, a special constant record is provided: {@link Record#END_OF_DATA}. This is
 * just a record that has an empty metadata, that is, it can't hold any data in it.
 * </p>
 * 
 * @author OPEN input - <a href="http://www.openinput.com/">http://www.openinput.com/</a>
 */
public class Record implements Comparable<Record>, Serializable, Cloneable {
    private static final long serialVersionUID = -5058506696243955825L;
    private static Log log = LogFactory.getLog(Record.class);
    private static ResourceBundle messages = ResourceBundle.getBundle(Record.class.getName());

    /**
     * Constant record used to signal end of data in a communication.
     */
    public static Record END_OF_DATA = new Record(new RecordMetadata(new LinkedList<FieldMetadata>()));

    private RecordMetadata metadata;
    private List<Field<? extends Serializable>> data;

    /**
     * Creates a record that can store the kind of data described by the supplied metadata.
     * 
     * @param metadata the metadata describing the data this record may hold
     * @throws NullPointerException if the supplied metadata is <code>null</code>.
     */
    public Record(RecordMetadata metadata) {
        this(metadata, false);
    }

    /**
     * Creates a record that can store the kind of data described by the supplied metadata, possibly setting all the fields to the
     * <code>null</code> value.
     * 
     * @param metadata the metadata describing the data this record may hold
     * @param nullFields <code>true</code> to indicate that all fields should be set to <code>null</code>.
     * @throws NullPointerException if the supplied metadata is <code>null</code>.
     */
    public Record(RecordMetadata metadata, boolean nullFields) {
        this.metadata = new RecordMetadata(metadata.getFieldMetadata());
        this.data = new ArrayList<Field<? extends Serializable>>(
                Collections.nCopies(this.metadata.getFieldCount(), (Field<Serializable>) null));
        if (nullFields) {
            this.nullFields(this.getMetadata().getFieldNames());
        }
    }

    /**
     * Returns the metadata associated with this record. The instance returned is a copy of the real metadata, so if you want to change
     * the metadata associated with this record, use one of the supplied mutator methods ({@link Record#remove(Collection)},
     * {@link Record#retain(Collection)} or {@link Record#add(Record)}).
     * 
     * @return the metadata associated with this record.
     */
    public RecordMetadata getMetadata() {
        return this.metadata.clone();
    }

    /**
     * Returns the field with the supplied name. More specifically, the field name is searched in the list of field metadata, and then
     * the field in the corresponding position is retrieved and returned. If no field metadata is found with the given name this method
     * throws an exception.
     * 
     * @param fieldName the name of the field to retrieve.
     * @return the field with the supplied name.
     * @throws NoSuchFieldNameException if no field metadata can be found with the specified name.
     */
    public Field<? extends Serializable> getField(String fieldName) {
        int fieldIndex = this.metadata.getFieldIndex(fieldName);
        if (fieldIndex != -1) {
            return this.data.get(fieldIndex);
        } else {
            String errorMessage = MessageFormat.format(Record.messages.getString("message.invalid.name"),
                    fieldName);
            if (Record.log.isInfoEnabled()) {
                Record.log.info(Record.messages.getString("exception.invalid.name") + errorMessage);
            }
            throw new NoSuchFieldNameException(errorMessage);
        }
    }

    /**
     * <p>
     * Sets the field with the specified name to the value provided. If no field metadata is found with the given name this method throws
     * an exception.
     * </p>
     * <p>
     * <b>WARNING:</b> This method doesn't currently check that the field provided holds a valid value taking into account the
     * corresponding field metadata. This should be fixed as soon as possible.
     * </p>
     * 
     * @param fieldName the name of the field to set.
     * @param field the new value of the field.
     * @throws NoSuchFieldNameException if no field metadata can be found with the specified name.
     */
    public void setField(String fieldName, Field<?> field) {
        // FIXME: Check the type of the value against the field metadata
        int fieldIndex = this.metadata.getFieldIndex(fieldName);
        if (fieldIndex != -1) {
            this.data.set(fieldIndex, field);
        } else {
            String errorMessage = MessageFormat.format(Record.messages.getString("message.invalid.name"),
                    fieldName);
            if (Record.log.isInfoEnabled()) {
                Record.log.info(Record.messages.getString("exception.invalid.name") + errorMessage);
            }
            throw new NoSuchFieldNameException(errorMessage);
        }
    }

    /**
     * Sets the fields of this record from the values contained in the provided record. The operation is done based on field names, so
     * fields of the same name are copied from the provided record to this record. Fields with names not included in this record are
     * ignored. The field order of this record is always preserved.
     * 
     * @param record the record which fields are copied to this record
     * @throws NullPointerException if the supplied record is <code>null</code>.
     */
    public void setFields(Record record) {
        for (FieldMetadata currentFieldMetadata : record.getMetadata().getFieldMetadata()) {
            String fieldName = currentFieldMetadata.getName();
            if (this.metadata.getFieldIndex(fieldName) != -1) {
                this.setField(fieldName, record.getField(fieldName));
            }
        }
    }

    /**
     * Sets all the fields with names contained in the provided collection to the <code>null</code> value. This means that the
     * corresponding holes in the record will be filled with a {@link Field} instance containing the <code>null</code> value. Fields
     * with names not included in this record are ignored.
     * 
     * @param fieldNames the name of the field to be set to the <code>null</code> value.
     * @throws NullPointerException if the supplied collection is <code>null</code>.
     */
    public void nullFields(Collection<String> fieldNames) {
        for (String fieldName : fieldNames) {
            if (this.metadata.getFieldIndex(fieldName) != -1) {
                this.setValue(fieldName, null);
            }
        }
    }

    /**
     * Returns the value held by the field with the supplied name. This is just a convenience method to save us from an extra method call
     * and cast.
     * 
     * @param <T> the desired type of the returned value.
     * @param clazz the desired type of the returned value.
     * @param fieldName the name of the field which value will be retrieved.
     * @return the value held by the field with the supplied name.
     * @throws NoSuchFieldNameException if no field metadata can be found with the specified name.
     * @throws ClassCastException if the type provided is not compatible with the real type of the requested value.
     */
    public <T extends Serializable> T getValue(Class<T> clazz, String fieldName) {
        int fieldIndex = this.metadata.getFieldIndex(fieldName);
        if (fieldIndex != -1) {
            Field<? extends Object> field = this.data.get(fieldIndex);
            return (T) field.getValue();
        } else {
            String errorMessage = MessageFormat.format(Record.messages.getString("message.invalid.name"),
                    fieldName);
            if (Record.log.isInfoEnabled()) {
                Record.log.info(Record.messages.getString("exception.invalid.name") + errorMessage);
            }
            throw new NoSuchFieldNameException(errorMessage);
        }
    }

    /**
     * <p>
     * Sets the field with the specified name to the value provided. If no field metadata is found with the given name this method throws
     * an exception.
     * </p>
     * <p>
     * <b>WARNING:</b> This method doesn't currently check that the value provided is a valid value taking into account the
     * corresponding field metadata. This should be fixed as soon as possible.
     * </p>
     * 
     * @param <T> the type of the value to set.
     * @param fieldName the name of the field to set.
     * @param value the new value of the field.
     * @throws NoSuchFieldNameException if no field metadata can be found with the specified name.
     */
    public <T extends Serializable> void setValue(String fieldName, T value) {
        int fieldIndex = this.metadata.getFieldIndex(fieldName);
        if (fieldIndex != -1) {
            this.data.set(fieldIndex, new Field<T>(value));
        } else {
            String errorMessage = MessageFormat.format(Record.messages.getString("message.invalid.name"),
                    fieldName);
            if (Record.log.isInfoEnabled()) {
                Record.log.info(Record.messages.getString("exception.invalid.name") + errorMessage);
            }
            throw new NoSuchFieldNameException(errorMessage);
        }
    }

    /**
     * Removes from this record all the field metadata and all the fields with names included in the supplied collection.
     * 
     * @param fieldNames the names of the field metadata and fields to remove.
     * @throws NullPointerException if the specified collection is <code>null</code>.
     */
    public void remove(Collection<String> fieldNames) {
        List<String> fieldsToRemove = new LinkedList<String>(this.metadata.getFieldNames());
        fieldsToRemove.retainAll(fieldNames);

        ListIterator<String> fieldIndexIterator = fieldsToRemove.listIterator(fieldsToRemove.size());
        while (fieldIndexIterator.hasPrevious()) {
            this.data.remove(fieldIndexIterator.previous());
        }

        this.metadata.remove(fieldNames);
    }

    /**
     * Retains all the field metadata and fields with names included in the suppled collection. In other words, removes from this record
     * all the field metadata and fields with names not included in the supplied collection.
     * 
     * @param fieldNames the names of the field metadata and fields to keep.
     * @throws NullPointerException if the specified collection is <code>null</code>.
     */
    public void retain(Collection<String> fieldNames) {
        List<String> fieldsToRemove = new LinkedList<String>(this.metadata.getFieldNames());
        fieldsToRemove.removeAll(fieldNames);
        this.remove(fieldsToRemove);
    }

    /**
     * Appends the field metadata and fields contained in the supplied record to the end of the field metadata and fields contained in
     * this record.
     * 
     * @param record the record which field metadata and fields will be appended to this record.
     * @throws NullPointerException if <code>record</code> is <code>null</code>.
     * @throws IllegalArgumentException if the the supplied record contains some field metadata with the same name that some field
     *           metadata in this record.
     */
    public void add(Record record) {
        this.metadata.add(record.getMetadata());
        this.data.addAll(record.data);
    }

    /**
     * Returns a record containing all the field metadata and fields of this record with names not included in the supplied collection.
     * 
     * @param fieldNames the names of the field metadata and fields to remove.
     * @return a record with the same field metadata and fields that this record, supressing the specified field metadata and fields.
     * @throws NullPointerException if the specified collection is <code>null</code>.
     */
    public Record supress(Collection<String> fieldNames) {
        List<String> fieldNamesToExtract = new LinkedList<String>(this.metadata.getFieldNames());
        fieldNamesToExtract.removeAll(fieldNames);
        return this.extract(fieldNamesToExtract);
    }

    /**
     * Returns a record containing all the field metadata and fields of this record with names included in the supplied list. The order
     * given in the supplied list is preserved in the resulting record.
     * 
     * @param fieldNames the names of the field metadata and fields to extract.
     * @return a record containing all the field metadata and fields in this record which field name is included in the supplied list.
     * @throws NullPointerException if the specified collection is <code>null</code>.
     * @throws IllegalArgumentException if the supplied list contains duplicated names.
     */
    public Record extract(List<String> fieldNames) {
        Record record = new Record(this.metadata.extract(fieldNames));
        for (String fieldName : fieldNames) {
            record.setField(fieldName, this.getField(fieldName));
        }

        return record;
    }

    /**
     * Returns a record containing the field metadata and fields of this record concatenated with the field metadata and fields contained
     * in the supplied record.
     * 
     * @param record the record which field metadata and fields will be appended to this record.
     * @return a record containing the concatenated field metadata and fields of this record and the supplied record.
     * @throws NullPointerException if <code>record</code> is <code>null</code>.
     * @throws IllegalArgumentException if the the supplied record contains some field metadata with the same name that some field
     *           metadata in this record.
     */
    public Record concatenate(Record record) {
        Record newRecord = new Record(this.getMetadata().concatenate(record.getMetadata()));
        newRecord.data = (List<Field<? extends Serializable>>) ((ArrayList<Field<? extends Serializable>>) this.data)
                .clone();
        newRecord.data.addAll(record.data);
        return newRecord;
    }

    /**
     * Compares the specified object with this record for equality. Returns <code>true</code> if and only if the specified object is
     * also a record, and the list of field metadata and fields are equal.
     * 
     * @param object The object to be compared for equality with this record.
     * @return <code>true</code> if the specified object is equal to this record.
     */
    @Override
    public boolean equals(Object object) {
        return this == object || (object instanceof Record && this.metadata.equals(((Record) object).metadata)
                && this.data.equals(((Record) object).data));
    }

    /**
     * Returns the hash code value for this record.
     * 
     * @return the hash code value for this record.
     */
    @Override
    public int hashCode() {
        return this.metadata.hashCode() ^ this.data.hashCode();
    }

    /**
     * Returns a copy of this record. A shallow copy of the private instance variables is done, so changes in the cloned record doesn't
     * affect the original instance.
     * 
     * @return a clone of this <code>RecordMetadata</code> instance
     */
    @Override
    public Record clone() {
        try {
            Record clonedRecord = (Record) super.clone();
            clonedRecord.metadata = (RecordMetadata) this.metadata.clone();
            clonedRecord.data = (List<Field<? extends Serializable>>) ((ArrayList<Field<? extends Serializable>>) this.data)
                    .clone();
            return clonedRecord;
        } catch (CloneNotSupportedException exc) {
            Record.log.fatal(Record.messages.getString("error.clone.not.supported"), exc);
            throw new InternalError();
        }
    }

    /**
     * Returns a string representation of this record. The string representation is a comma separated list of values surrounded by square
     * brackets.
     * 
     * @return a string representation of this record.
     */
    @Override
    public String toString() {
        StringBuffer recordString = new StringBuffer("[");
        for (Field<? extends Object> field : this.data) {
            recordString.append(field.toString());
            recordString.append(',');
        }
        if (!this.data.isEmpty()) {
            recordString.deleteCharAt(recordString.length() - 1);
        }
        recordString.append(']');
        return recordString.toString();
    }

    /**
     * <p>
     * Compares this record with the specified record for order. The order is determined comparing the records field by field preserving
     * the order specified by the metadata. In order to compare two records, they must have the same metadata, if they don't a
     * <code>ClassCastException</code> is thrown. This exception is also thrown if some of the values in this record doesn't implement
     * <code>Comparable</code>.
     * </p>
     * <p>
     * While comparing a <code>null</code> value is considered to be equal to another <code>null</code> value, and less than any
     * other value.
     * </p>
     * 
     * @param record the record to be compared.
     * @return a negative integer, zero, or a positive integer as this record is less than, equal to, or greater than the specified
     *         record.
     * @throws ClassCastException if the metadata aren't equal, so it's impossible to compare the records.
     * @throws ClassCastException if some of the values in this record doesn't implement <code>Comparable</code>.
     * @see java.lang.Comparable#compareTo(T)
     */
    public int compareTo(Record record) {
        int result = 0;
        if (this.getMetadata().equals(record.getMetadata())) {
            // TODO: Improve this a bit: shortcut in case this == record
            for (int fieldIndex = 0; fieldIndex < this.data.size() && result == 0; fieldIndex++) {
                Object currentValue = this.data.get(fieldIndex).getValue();
                Object currentOtherValue = record.data.get(fieldIndex).getValue();
                if (currentValue == null || currentOtherValue == null) {
                    result = (currentValue == currentOtherValue) ? 0 : ((currentValue == null) ? -1 : 1);
                } else if (currentValue instanceof Comparable) {
                    result = ((Comparable<Object>) currentValue).compareTo(currentOtherValue);
                } else {
                    String errorMessage = MessageFormat.format(
                            Record.messages.getString("message.noncomparable.data"), currentValue,
                            currentValue.getClass(), this);
                    if (Record.log.isInfoEnabled()) {
                        Record.log.info(Record.messages.getString("exception.comparing.records") + errorMessage);
                    }
                    throw new ClassCastException(errorMessage);
                }
            }
            return result;
        } else {
            String errorMessage = MessageFormat.format(Record.messages.getString("message.incompatible.metadata"),
                    this.getMetadata(), record.getMetadata());
            if (Record.log.isInfoEnabled()) {
                Record.log.info(Record.messages.getString("exception.comparing.records") + errorMessage);
            }
            throw new ClassCastException(errorMessage);
        }
    }
}