com.prowidesoftware.swift.model.field.Field.java Source code

Java tutorial

Introduction

Here is the source code for com.prowidesoftware.swift.model.field.Field.java

Source

/*******************************************************************************
 * Copyright (c) 2016 Prowide Inc.
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as 
 *     published by the Free Software Foundation, either version 3 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.  
 *     
 *     Check the LGPL at <http://www.gnu.org/licenses/> for more details.
 *******************************************************************************/
package com.prowidesoftware.swift.model.field;

import java.lang.reflect.Constructor;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Currency;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.UUID;
import java.util.logging.Level;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.time.DateFormatUtils;

import com.prowidesoftware.swift.DeleteSchedule;
import com.prowidesoftware.swift.io.writer.FINWriterVisitor;
import com.prowidesoftware.swift.model.BIC;
import com.prowidesoftware.swift.model.Tag;
import com.prowidesoftware.swift.utils.SwiftFormatUtils;

/**
 * Base class implemented by classes that provide a general access to field components.
 *
 * @author www.prowidesoftware.com
 * @since 6.0
 */
public abstract class Field implements PatternContainer {
    private static final transient java.util.logging.Logger log = java.util.logging.Logger
            .getLogger(Field.class.getName());

    /**
     * Zero based list of field components in String format.<br />
     * For example: for field content ":FOO//EUR1234 will be components[0]=FOO, components[1]=EUR and components[1]=1234
     */
    protected List<String> components;

    /**
     * @deprecated usar {@link #Field(int)}
     */
    @Deprecated
    protected Field() {
    }

    /**
     * Creates a field with the list of components initialized to the given number of components.
     * @see #init(int)
     * @param components the number of components to initialize
     */
    protected Field(final int components) {
        super();
        init(components);
    }

    /**
     * Initialize the list of components to the indicated size and sets all values to <code>null</code>
     * @param components the number of components to initialize
     * @since 7.8
     */
    protected void init(final int components) {
        this.components = new ArrayList<String>(components);
        for (int i = 0; i < components; i++) {
            this.components.add(null);
        }
    }

    /**
     * Creates a new field and initializes its components with content from the parameter value.
     * The value is parsed with {@link #parse(String)}     
     * @param value complete field value including separators and CRLF
     */
    protected Field(final String value) {
        super();
        parse(value);
    }

    /**
     * Parses the parameter value into the internal components structure.
     * Used to update all components from a full new value, as an alternative
     * to setting individual components. Previous components value is overwritten.
     * <br>
     * Implemented by subclasses with logic for each specific field structure. 
     * 
     * @param value complete field value including separators and CRLF
     * @since 7.8
     */
    /*
     * sebastian oct 2015
     * Este metodo debe ser abstract. No lo es ahroa solamente por compatibilidad
     * con SRU2014 donde el parseo esta impementado en el constructor. Esto puede
     * ponerse como abstract en caso de regenerar con codegen fields de SRU2014 o
     * bien cuando se implemente SRU2016 y quede deprecado SRU2014.
     */
    public void parse(final String value) {
    };

    /**
     * Copy constructor.<br>
     * Initializes the components list with a deep copy of the source components list.
     * @param source a field instance to copy
     * @since 7.7
     */
    protected Field(final Field source) {
        this.components = new ArrayList<String>(source.getComponents());
    }

    /**
     * Implementation of toString using ToStringBuilder from commons-lang
     */
    @Override
    public String toString() {
        return org.apache.commons.lang.builder.ToStringBuilder.reflectionToString(this);
    }

    /**
     * Implementation of equals using EqualsBuilder from commons-lang
     */
    @Override
    public boolean equals(final Object obj) {
        return org.apache.commons.lang.builder.EqualsBuilder.reflectionEquals(this, obj);
    }

    /**
     * Implementation of hashCode using HashCodeBuilder from commons-lang
     */
    @Override
    public int hashCode() {
        return org.apache.commons.lang.builder.HashCodeBuilder.reflectionHashCode(this);
    }

    /**
     * Format the given object as a money number without currency information in format
     * @param aValue
     * @return the formatted amount as String
     */
    protected static String formatNumber(final Object aValue) {
        //create formatter for financial amounts
        final DecimalFormat fmt = new DecimalFormat("#,###.00");

        final NumberFormat f = NumberFormat.getInstance(Locale.getDefault());
        if (f instanceof DecimalFormat) {
            ((DecimalFormat) f).setDecimalSeparatorAlwaysShown(true);
            fmt.setDecimalFormatSymbols(((DecimalFormat) f).getDecimalFormatSymbols());
        }
        final String formatted = fmt.format(aValue);
        return formatted;
    }

    /**
     * @param d Date object to format
     * @return the formatted date as dd/MM/yyyy or empty if exception occurs during formatting
     */
    protected static String format(final Calendar d) {
        try {
            return DateFormatUtils.format(d.getTime(), "dd/MM/yyyy");
        } catch (final Exception ignored) {
            return StringUtils.EMPTY;
        }
    }

    /**
     * A formatted amount with a fixed format nnnn-nnnnn-nnn-n
     * @param a string with an account number or <code>null</code>
     * @return the formatted account or an empty String if param is <code>null</code>
     */
    // TODO support user formatting masks from property file
    protected static String formatAccount(final String a) {
        if (a == null) {
            //be gentle with null
            return StringUtils.EMPTY;
        }
        final StringBuilder result = new StringBuilder(a);
        try {
            result.insert(4, '-');
            result.insert(9, '-');
            result.insert(12, '-');
        } catch (final Exception ignored) {
        }
        return result.toString();
    }

    /**
     * Append each lines in a new lines, empty lines are ignored
     * @param sb must not be null, target buffer
     * @param lines may be null or empty, nothing is done in this case
     */
    protected void appendInLines(final StringBuilder sb, final String... lines) {
        Validate.notNull(sb);
        if (lines == null) {
            log.finest("lines is null");
        } else {
            for (int i = 0; i < lines.length; i++) {
                if (StringUtils.isNotBlank(lines[i])) {
                    if ((i != 0) || ((i == 0) && StringUtils.isNotBlank(sb.toString()))) {
                        sb.append(FINWriterVisitor.SWIFT_EOL);
                    }
                    sb.append(lines[i]);
                }
            }
        }
    }

    /**
     * Append each component between componentStart and componentEnd in a new lines, empty components are ignored
     * @param sb must not be null, target buffer
     * @param componentStart starting component number to add
     * @param componentEnd ending component number to add
     */
    protected void appendInLines(final StringBuilder sb, final int componentStart, final int componentEnd) {
        Validate.notNull(sb);
        boolean first = true;
        for (int i = componentStart; i <= componentEnd; i++) {
            final String c = getComponent(i);
            if (StringUtils.isNotBlank(c)) {
                if (!first || StringUtils.isNotBlank(sb.toString())) {
                    sb.append(FINWriterVisitor.SWIFT_EOL);
                }
                sb.append(c);
                first = false;
            }
        }
    }

    /**
     * @return comopnents list
     */
    public List<String> getComponents() {
        return components;
    }

    /**
     * @param components list to set
     */
    public void setComponents(final List<String> components) {
        this.components = components;
    }

    /**
     * Inserts a component String value into the list of components, using the component number to position the value into the List.
     * @param number component number, first component of a field should be number one
     * @param value String value of the parsed component (without component separators ':', '/', '//')
     */
    public void setComponent(final int number, final String value) {
        Validate.isTrue(number > 0, "component number is 1-based");

        //internal position index is zero based
        final int position = number - 1;

        if (this.components == null) {
            this.components = new ArrayList<String>();
        }
        if (position >= 0) {
            if (position >= this.components.size()) {
                //TODO deal with this error
            } else {
                this.components.set(position, value);
            }
        } else {
            log.severe("components are named starting at 1, cannot insert a component with number " + number);
        }
    }

    /**
     * Gets a specific component from the components list.
     * @param number one-based index of component, first component of a field should be number one
     * @return found component or <code>null</code>
     */
    public String getComponent(final int number) {
        //internal position index is zero based
        final int position = number - 1;

        if (this.components != null) {
            if ((position >= 0) && (position < this.components.size())) {
                return this.components.get(position);
            }
        }
        return null;
    }

    /**
     * @see #getValueDisplay(Locale)
     */
    public String getValueDisplay() {
        return getValueDisplay(null);
    }

    /**
     * Get a localized, suitable for showing to humans string of the field values.
     * This method is overwritten when necessary by subclasses.
     * 
     * @param locale optional locale to format date and amounts, if null, the default locale is used
     * @return a concatenation of formated components with " " separator
     * @see #getValueDisplay(int, Locale)
     * @since 7.8
     */
    public String getValueDisplay(Locale locale) {
        final StringBuilder result = new StringBuilder();
        for (int i = 1; i <= components.size(); i++) {
            final String s = getValueDisplay(i, locale);
            if (s != null) {
                if (result.length() > 0) {
                    result.append(" ");
                }
                result.append(s);
            }
        }
        return result.toString();
    }

    /**
     * Returns a localized suitable for showing to humans string of a field component.
     *
     * @param component number of the component to display
     * @param locale optional locale to format date and amounts, if null, the default locale is used
     * @return formatted component value or null if component number is invalid or not present
     * @throws IllegalArgumentException if component number is invalid for the field
     * @since 7.8
     */
    public abstract String getValueDisplay(int component, Locale locale);

    /**
     * Get the given component as the given object type.
     * If the class is not recognized, it returns null, as well as if conversion fails.
     * @param component one-based index of the component to retrieve
     * @see #getComponent(int)
     * @throws IllegalArgumentException if c is not any of: String, BIC, Currency, Number, BigDecimal Character or Integer
     */
    public Object getComponentAs(final int component, @SuppressWarnings("rawtypes") final Class c) {
        try {
            final String s = getComponent(component);
            log.finest("converting string value: " + s);

            if (c.equals(String.class)) {
                return s;

            } else if (c.equals(Number.class) || c.equals(BigDecimal.class)) {
                return SwiftFormatUtils.getNumber(s);

            } else if (c.equals(BIC.class)) {
                return new BIC(s);

            } else if (c.equals(Currency.class)) {
                return Currency.getInstance(s);

            } else if (c.equals(Character.class)) {
                return SwiftFormatUtils.getSign(s);

            } else if (c.equals(Integer.class)) {
                return Integer.valueOf(s);

            } else {
                throw new IllegalArgumentException("Can't handle " + c.getName());
            }
        } catch (final Exception e) {
            log.severe("Error converting component content: " + e);
        }
        return null;
    }

    /**
     * Get the given component as a number object
     * This method internall y calls {@link #getComponentAs(int, Class)}, and casts the result
     * @since 7.8
     */
    public Number getComponentAsNumber(final int component) {
        return (Number) getComponentAs(component, Number.class);
    }

    /**
     * Returns a string with joined components values.
     *
     * @param start starting index of components to join (zero based)
     * @param skipLast if true the last component will not be included in the join, and where
     * the "last" component is understood as the last not empty component (this is not necessary
     * the last component of the field's component list.
     *
     * @return s
     */
    public String joinComponents(final int start, final boolean skipLast) {
        // FIXME para que se crea el list intermedio toAdd? no le veo razon de ser, se podria iterar en el segundo loop directo sobre this.components
        final List<String> toAdd = new ArrayList<String>();
        for (int i = start; i < this.componentsSize(); i++) {
            if (StringUtils.isNotEmpty(this.components.get(i))) {
                toAdd.add(this.components.get(i));
            }
        }
        final int end = skipLast ? toAdd.size() - 1 : toAdd.size();
        final StringBuilder result = new StringBuilder();
        for (int i = 0; i < end; i++) {
            result.append(toAdd.get(i));
        }
        return result.toString();
    }

    /**
     * Returns a string with all field's components joined.
     * @param skipLast
     * @return s
     * @see #joinComponents(int, boolean)
     */
    public String joinComponents(final boolean skipLast) {
        return joinComponents(0, skipLast);
    }

    /**
     * Returns a string with all field's components joined
     * @param start
     * @return s
     * @see #joinComponents(int, boolean)
     */
    public String joinComponents(final int start) {
        return joinComponents(start, false);
    }

    /**
     * Returns a string with all field's components joined.
     * @return s
     * @see #joinComponents(int, boolean)
     */
    public String joinComponents() {
        return joinComponents(0, false);
    }

    /**
     * Gets a BigDecimal from a generic Number argument
     * @param number
     * @return BigDecimal value of number parameter
     */
    static public BigDecimal getAsBigDecimal(final Number number) {
        if (number instanceof BigDecimal) {
            return (BigDecimal) number;
        } else if (number instanceof Long) {
            return new BigDecimal(((Long) number).longValue());
        } else if (number instanceof Integer) {
            return new BigDecimal(((Integer) number).intValue());
        } else if (number instanceof Short) {
            return new BigDecimal(((Short) number).intValue());
        } else if (number instanceof Double) {
            return new BigDecimal(number.toString());
        } else {
            throw new IllegalArgumentException("class " + number.getClass().getName() + " is not supported");
        }
    }

    /**
     * Returns the first component starting with the given prefix value or <code>null</code> if not found.
     * @param prefix
     * @return s
     */
    public String findComponentStartingWith(final String prefix) {
        for (int i = 0; i < this.components.size(); i++) {
            final String c = this.components.get(i);
            if (StringUtils.startsWith(c, prefix)) {
                return c;
            }
        }
        return null;
    }

    /**
     * Finds the first component starting with the given codeword between slashes, and returns the component subvalue.
     * For example, for the following field value<br />
     * /ACC/BLABLABLA CrLf<br />
     * //BLABLABLA CrLf<br />
     * /INS/CITIUS33MIA CrLf<br />
     * //BLABLABLA CrLf<br />
     * A call to this method with parameter "INS" will return "CITIUS33MIA"
     *
     * @param codeword
     * @see #findComponentStartingWith(String)
     * @return the found value or <code>null</code> if not found
     */
    public String getValueByCodeword(final String codeword) {
        final String key = "/" + codeword + "/";
        final String c = findComponentStartingWith(key);
        if (c != null) {
            return StringUtils.substringAfter(c, key);
        }
        return null;
    }

    /**
     * Serializes the fields' components into the single string value (SWIFT format).
     * Must be overwritten by by subclasses.
     * @return SWIFT formatted value
     */
    public String getValue() {
        return joinComponents();
    }

    /**
     * Returns true if all field's components are blank or null
     * @return true if all field's components are blank or null
     */
    public boolean isEmpty() {
        for (final String c : getComponents()) {
            if ((c != null) && StringUtils.isNotBlank(c)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Creates a Field instance for the given Tag object, using reflection.
     * The created object is populated with parsed components data from the Tag.
     * @param t a tag with proper name and value content
     * @return a specific field object, ex: Field32A. Or <code>null</code> if exceptions occur during object creation.
     */
    static public Field getField(final Tag t) {
        return getField(t.getName(), t.getValue());
    }

    /**
     * Creates a Field instance for the given it's name and and optional value, using reflection.
     * 
     * @param name a proper field name, ex: 32A, 22F, 20
     * @param value an optional field value or <code>null</code> to create the field with no initial content
     * @return a specific field object, ex: Field32A. Or <code>null</code> if exceptions occur during object creation.
     * @since 7.8
     */
    static public Field getField(final String name, final String value) {
        Object r = null;
        try {
            final Class<?> c = Class.forName("com.prowidesoftware.swift.model.field.Field" + name);
            @SuppressWarnings("rawtypes")
            final Class[] argsClass = new Class[] { String.class };
            @SuppressWarnings("rawtypes")
            final Constructor ct = c.getConstructor(argsClass);
            final Object arglist[] = new Object[] { value };
            r = ct.newInstance(arglist);
        } catch (final ClassNotFoundException e) {
            log.log(Level.WARNING, "Field class for Field" + name
                    + " not found. This is normally caused by an unrecognized field in the message. Please check the structure validation problems reported by the validation.",
                    e);
        } catch (final Exception e) {
            log.log(Level.WARNING, "An error occured while creating an instance of " + name, e);
        }
        return (Field) r;

    }

    /**
      * @deprecated field labels varies depending on the specific MT and sequence, label should be retrieve using {@link #getLabel(String, String, String, Locale)} with proper MT and sequence identifiers
     */
    @Deprecated
    @DeleteSchedule(2018)
    public String getLabel() {
        return getLabel(Locale.getDefault());
    }

    /**
     * @deprecated field labels varies depending on the specific MT and sequence, label should be retrieve using {@link #getLabel(String, String, String, Locale)} with proper MT and sequence identifiers
     */
    @Deprecated
    @DeleteSchedule(2018)
    public String getLabel(final Locale locale) {
        return getLabel(getName(), locale);
    }

    /**
     * @deprecated field labels varies depending on the specific MT and sequence, label should be retrieve using {@link #getLabel(String, String, String, Locale)} with proper MT and sequence identifiers
     */
    @Deprecated
    @DeleteSchedule(2018)
    static public String getLabel(final String fieldName, final Locale locale) {
        return _getLabel(fieldName, null, null, locale);
    }

    /*
     * Legacy implementation for backward compatibility
     * This method is used only by deprecated label API, to maintain the old version of labels.
     * 
     * The usage of this deprecated bundle and labels API is discourage because labels are context dependent, meaning
     * the proper label for a field depends on the MT at least, and in some occasions also depends on the particular 
     * sequence.
     * 
     * The new bundles include proper names for each combination of field name, MT and sequences as needed. There are
     * small subset of fields sharing the same naming cross MTs and cross sequences, but most of the new labels include
     * variations per MT and in several cases per sequence.
     */
    @Deprecated
    @DeleteSchedule(2018)
    static private String _getLabel(final String fieldName, final String mt, final String sequence,
            final Locale locale) {
        final String bundle = "deprecated_labels";
        String key = null;
        String result = null;
        //try {
        final ResourceBundle labels = ResourceBundle.getBundle(bundle, locale);
        if (labels != null) {
            if ((sequence != null) && (mt != null)) {
                key = "field" + fieldName + "[" + mt + "][" + sequence + "].name";
                result = getString(labels, key);
                if (result == null) {
                    key = "field" + getNumber(fieldName) + "a[" + mt + "][" + sequence + "].name";
                    result = getString(labels, key);
                }
            }
            if ((result == null) && (mt != null)) {
                key = "field" + fieldName + "[" + mt + "][" + sequence + "].name";
                result = getString(labels, key);
                if (result == null) {
                    key = "field" + getNumber(fieldName) + "a[" + mt + "].name";
                    result = getString(labels, key);
                }
            }
            if (result == null) {
                key = "field" + fieldName + ".name";
                result = getString(labels, key);
                if (result == null) {
                    key = "field" + getNumber(fieldName) + "a.name";
                    result = getString(labels, key);
                }
            }
            if (result == null) {
                key = "field" + getNumber(fieldName) + ".name";
                result = getString(labels, key);
            }
        }
        //} catch (MissingResourceException e) {
        //   e.printStackTrace();
        //}
        if (result != null) {
            return result;
        }
        return key;
    }

    /**
     * Same as {@link #getLabel(String, String, String, Locale)} using default locale
     * @since 7.8
     */
    static public String getLabel(final String fieldName, final String mt, final String sequence) {
        return getLabel(fieldName, mt, sequence, Locale.getDefault());
    }

    /**
     * Returns the field business description name, using resource bundle from pw_swift_labels property files.
     * Field names may be generic for all usages, or may differ for particular letter option, message type
     * or even sequence of a message type. The property supports all this kind of definitions with generic
     * labels and specific ones. The following example illustrate the precedence of bundle keys that are checked for
     * field 50:<br />
     * <ul>
     * <li>50K[103][B]</li>
     * <li>50a[103][B]</li>
     * <li>50K[103]</li>
     * <li>50a[103]</li>
     * <li>50K</li>
     * <li>50a</li>
     * <li>50</li>
     * </ul>
     * 
     * @param fieldName field name of the field to retrieve its label, if the combination of number and letter option
     * is provided then a specific label is returned; is the letter option is omitted then a more generic label is returned.
     * @param mt optional indication of message type or <code>null</code>.
     * @param sequence optional indication of sequence or <code>null</code> if does not apply for the specific MT and field.
     * @param locale the locale for which a resource bundle is desired
     *
     * @return a resource bundle based label for the given locale or the tag name, or the resource key if not found
     */
    static public String getLabel(final String fieldName, final String mt, final String sequence,
            final Locale locale) {
        return _getLabel(fieldName, mt, sequence, locale, "name");
    }

    /**
     * Similar to {@link #getLabelComponents(String, String, String, Locale)} but returning the components property in bundle
     * @since 7.8.4
     */
    static public String getLabelComponents(final String fieldName, final String mt, final String sequence,
            final Locale locale) {
        Locale l = locale != null ? locale : Locale.getDefault();
        return _getLabel(fieldName, mt, sequence, l, "components");
    }

    static private String _getLabel(final String fieldName, final String mt, final String sequence,
            final Locale locale, final String prop) {
        final String bundle = "pw_swift_labels";
        String key = null;
        String result = null;
        //try {
        final ResourceBundle labels = ResourceBundle.getBundle(bundle, locale);
        if (labels != null) {
            if ((sequence != null) && (mt != null)) {
                /*
                 * sequence + mt
                 */
                key = "field" + fieldName + "[" + mt + "][" + sequence + "]." + prop;
                result = getString(labels, key);
                if (result == null) {
                    /*
                     * sequence + mt + generic letter option
                     */
                    key = "field" + getNumber(fieldName) + "a[" + mt + "][" + sequence + "]." + prop;
                    result = getString(labels, key);
                }
            }
            if ((result == null) && (mt != null)) {
                /*
                 * mt only
                 */
                key = "field" + fieldName + "[" + mt + "]." + prop;
                result = getString(labels, key);
                if (result == null) {
                    /*
                     * mt + generic letter option
                     */
                    key = "field" + getNumber(fieldName) + "a[" + mt + "]." + prop;
                    result = getString(labels, key);
                }
            }
            if (result == null) {
                /*
                 * tag only
                 */
                key = "field" + fieldName + "." + prop;
                result = getString(labels, key);
                if (result == null) {
                    /*
                     * tag + generic letter option
                     */
                    key = "field" + getNumber(fieldName) + "a." + prop;
                    result = getString(labels, key);
                }
            }
            if (result == null) {
                /*
                 * number only
                 */
                key = "field" + getNumber(fieldName) + "." + prop;
                result = getString(labels, key);
            }
        }
        //} catch (MissingResourceException e) {
        //   e.printStackTrace();
        //}
        if (result != null) {
            return result;
        }
        return key;
    }

    /**
     * Helper implementation of getString from bundle without throwing exception
     * @param labels
     * @param key
     * @return the found resource or <code>null</code> if not found for the given key
     */
    private static String getString(final ResourceBundle labels, final String key) {
        try {
            return labels.getString(key);
        } catch (final MissingResourceException e) {
            return null;
        }
    }

    private static String getNumber(final String fieldName) {
        if (fieldName != null) {
            final StringBuilder sb = new StringBuilder();
            for (int i = 0; i < fieldName.length(); i++) {
                final char c = fieldName.charAt(i);
                if (Character.isDigit(c)) {
                    sb.append(c);
                }
            }
            if (sb.length() > 0) {
                return sb.toString();
            }
        }
        return null;
    }

    /**
     * Returns the field's name composed by the field number and the letter option (if any)
     * @return the static value of FieldNN.NAME
     */
    public abstract String getName();

    /**
     * Returns the field's components pattern
     * @since 7.8
     */
    public abstract String componentsPattern();

    /**
     * Returns the field's validator pattern
     * @since 7.8
     */
    public abstract String validatorPattern();

    public abstract boolean isOptional(int component);

    public abstract boolean isGeneric();

    /**
     * Moved to GenericField Interface
     */
    @Deprecated
    public String getDSS() {
        return null;
    }

    /**
     * Moved to GenericField Interface
     */
    @Deprecated
    public boolean isDSSPresent() {
        return false;
    }

    /**
     * Moved to GenericField Interface
     */
    @Deprecated
    public String getConditionalQualifier() {
        return null;
    }

    // FIXME debido a esto: el nombre del field deberia ser validado y eliminado como atributo dinamico
    /**
     * Return the letter option of this field as given by it classname or <code>null</code> if this field has no letter option
     */
    public Character letterOption() {
        final String cn = getClass().getName();
        final char c = cn.charAt(cn.length() - 1);
        if (Character.isLetter(c)) {
            return c;
        }
        return null;
    }

    /**
     * Tell if this field is of a given letter option.
     * letter is case sensitive
     */
    public boolean isLetterOption(final char c) {
        final Character l = letterOption();
        if (l != null) {
            return l.charValue() == c;
        }
        return false;
    }

    /**
     *
     * @param names must not be null nor empty
     * @return <code>true</code> if this field names equals one in the list of names and <code>false</code>
     *  in other case
     * @throws IllegalArgumentException if names is null or empty
     * @deprecated confusing name, use {@link #isNameAnyOf(String...)} instead
     * @see #isNameAnyOf(String...)
     */
    @Deprecated
    public boolean isAnyOf(final String... names) {
        Validate.isTrue(names != null && names.length > 0, "name list must have at least one element");
        for (final String n : names) {
            if (StringUtils.equals(getName(), n)) {
                return true;
            }
        }
        return false;
    }

    /**
     *
     * @param names must not be null nor empty
     * @return <code>true</code> if this field names equals one in the list of names and <code>false</code>
     *  in other case
     * @throws IllegalArgumentException if names is null or empty
     *
     */
    public boolean isNameAnyOf(final String... names) {
        Validate.isTrue(names != null && names.length > 0, "name list must have at least one element");
        for (final String n : names) {
            if (StringUtils.equals(getName(), n)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Compare the value of the component1 of this field with <code>compare</code>
     * Same as <code>is(1, compare)</code>
     */
    public boolean is(final String compare) {
        return StringUtils.equals(compare, getComponent(1));
    }

    /**
     * Compare the value of the component <code>componentNumber</code>  of this field with <code>compare</code>
     */
    public boolean is(final int componentNumber, final String compare) {
        return StringUtils.equals(compare, getComponent(componentNumber));
    }

    /**
     * Compare the value of the component1 of this field with <code>compare1</code> and the value of component2 with <code>compare2</code>
     */
    public boolean is(final String compare1, final String compare2) {
        return StringUtils.equals(compare1, getComponent(1)) && StringUtils.equals(compare2, getComponent(2));
    }

    /**
     * Get the generic tag object of this field.
     */
    public Tag asTag() {
        return new Tag(getName(), getValue());
    }

    /**
     * Returns the defined amount of components.<br>
     * This is not the amount of components present in the field instance, but the total amount of components
     * that this field accepts as defined.
     */
    public abstract int componentsSize();

    /**
     * Base implementation for subclasses getLine API.
     *
     * Notice that line instance numbers are static and relevant to the
     * field components definition, and not relative to the particular
     * instance value. For example field 35B Line[1] will be the line with the
     * ISIN number, regardless of the ISIN number present or not in the particular
     * field instance. If that ISIN line is not present in the parameter field,
     * the method will return null.<br><br>
     *
     * Also notice that a line may be composed by several components, there is
     * no linear relation between component numbers and lines numbers.<br>
     *
     * @param cp a copy of the subclass (this object is altered during method execution)
     * @param start a reference to a specific line in the field, first line being 1; if null returns all found lines.
     * @param end a reference to a specific line in the field, first line being 1; if null returns all found lines.
     * @param offset an optional component number used as offset when counting lines
     * @return found line content or null
     */
    protected String getLine(final Field cp, final Integer start, final Integer end, final int offset) {
        final String hash = UUID.randomUUID().toString();
        for (int i = 1; i <= componentsSize(); i++) {
            if (i < offset) {
                /*
                 * blank fields below the offset
                 */
                cp.setComponent(i, null);
            } else if (getComponent(i) == null) {
                /*
                 * fill empty components above offset
                 */
                cp.setComponent(i, hash);
            }
        }

        /*
         * get all meaningful lines from value
         */
        final List<String> lines = new ArrayList<String>();
        for (final String l : SwiftParseUtils.getLines(cp.getValue())) {
            if (StringUtils.isNotEmpty(l) && !onlySlashes(l)) {
                lines.add(l);
            }
        }

        /*
         * if the query includes a component offset, we remove meaningless prefix separators from result.
         */
        boolean removeSeparators = offset > 0;
        if (start != null) {
            if (lines.size() >= start) {
                if (end != null) {
                    if (end >= start) {
                        /*
                         * return line subset
                         */
                        return asString(hash, lines.subList(start - 1, end), removeSeparators);
                    } else {
                        log.warning("invalid lines range [" + start + "-" + end + "] the ending line number (" + end
                                + ") must be greater or equal to the starting line number (" + start + ")");
                    }
                } else {
                    /*
                     * return a single line
                     */
                    return clean(hash, lines.get(start - 1), removeSeparators);
                }
            }
        } else {
            /*
             * return all lines from offset
             */
            return asString(hash, lines, removeSeparators);
        }
        return null;
    }

    /**
     * Returns true if the value only contains '/' characters
     * (one or many but only that character)
     */
    private boolean onlySlashes(final String value) {
        for (int i = 0; i < value.length(); i++) {
            if (value.charAt(i) != '/') {
                return false;
            }
        }
        return true;
    }

    /**
     * Creates a string from the list of lines, replacing the hash by blank, and ignoring empty lines
     * @param hash hash used during getLine process
     * @param list list of lines
     * @param removeSeparators true to remove meaningless prefix separators, 
     * @return a string with the final, cleaned, joined lines
     */
    private String asString(final String hash, final List<String> list, boolean removeSeparators) {
        final StringBuilder result = new StringBuilder();
        for (int i = 0; i < list.size(); i++) {
            final String l = list.get(i);
            boolean b = i == 0 ? removeSeparators : false; //remove prefix only for first line
            final String trimmed = clean(hash, l, b);
            if (trimmed != null) {
                if (result.length() > 0) {
                    result.append(FINWriterVisitor.SWIFT_EOL);
                }
                result.append(trimmed);
            }
        }
        if (result.length() == 0) {
            return null;
        } else {
            return result.toString();
        }
    }

    /**
     * Replaces the hash by empty and trims to null.<br>
     *
     * It also can remove meaningless component separators (If the resulting string only
     * contains the component separator "/", or starts with ":" or starts with "/" separators
     * all of them will also be removed).
     *
     * @param hash hash string used by the get lines method
     * @param value current value to clean
     * @param removeSeparators if true, meaningless starting separators (: and /) are removed
     * @return proper final line value or null if the original field didn't contained content for such line
     */
    private String clean(final String hash, final String value, boolean removeSeparators) {
        /*
         * try to replace /hash first just in case the component is optional
         * then replace the hash only if present
         */
        String trimmed = StringUtils
                .trimToNull(StringUtils.replace(StringUtils.replace(value, "/" + hash, ""), hash, ""));
        if (trimmed != null && !onlySlashes(trimmed)) {
            /*
             * sebastian Oct 2015
             * La logica para remover separadores debiera depender de si el offset
             * abarca la linea entera o si el componente del offset esta en al mitad 
             * de una linea, y removerlo solo en este ultimo caso.
             * Esto es dificil de implementar porque no esta modelada la relacion
             * entre componentes y lineas.
             * Por lo tanto de momento se deja el parametro removeSeparators pero
             * con el codigo de aca abajo comentado. Y se coloca a cambio el patch
             * para el caso especifico de :// que es el que aparentemente no esta
             * contemplado segun los test.
             * 
            if (removeSeparators) {
               for (int i = 0; i < trimmed.length(); i++) {
                  char c = trimmed.charAt(i);
                  if (c != ':' && c != '/') {
              return trimmed.substring(i);
                  }
               }
            } else {
               return trimmed;
            }
            */
            if (trimmed.startsWith("://")) {
                return StringUtils.substringAfter(trimmed, "://");
            } else if (removeSeparators && (trimmed.startsWith(":") || trimmed.startsWith("/"))) {
                return StringUtils.trimToNull(StringUtils.substring(trimmed, 1));
            } else {
                return trimmed;
            }
        }
        /*
         * otherwise return null
         */
        return null;
    }

    /**
     * Returns true if the field name is valid. 
     * Valid field names are for example: 20, 20C, 108
     * @param name a field name to validate
     * @return true if valid, false otherwise
     * @since 7.8
     */
    public static boolean validName(final String name) {
        if (name == null) {
            return false;
        }
        if (name.length() < 2 || name.length() > 3) {
            //log.warning("field name must be present and have 2 or 3 characters length and found: "+field);
            return false;
        }
        if (!StringUtils.isNumeric(name.substring(0, 2))) {
            //log.warning("field name should start with a numeric prefix and found: "+field.substring(0, 2));
            return false;
        }
        if ((name.length() == 3 && !(Character.isDigit(name.charAt(2)) || name.charAt(2) == 'a'
                || Character.isUpperCase(name.charAt(2))))) {
            //log.warning("letter option if present should be a single capital letter or an 'a' for all letter options, and found: "+field.charAt(2));
            return false;
        }
        return true;
    }

    /**
     * Returns english label for components.
     * <br />
     * The index in the list is in sync with specific field component structure.
     * @see #getComponentLabel(int)
     * @since 7.8.4
     */
    protected abstract List<String> getComponentLabels();

    /**
     * Returns english label for the component.
     * <br />
     * @param number one-based index of component, first component of a field should be number one
     * @return found label or <code>null</code> if it is not defined
     * @since 7.8.4
     */
    public String getComponentLabel(final int number) {
        //internal position index is zero based
        final int position = number - 1;
        final List<String> labels = getComponentLabels();
        if (labels != null) {
            if ((position >= 0) && (position < labels.size())) {
                return labels.get(position);
            }
        }
        return null;
    }

    /*
     * TO DO: 
     * this will take the result of getLabelComponents
     * and use that as key to access bundle with translations.
     * For example Name And Address will be name-and-address key
     * in resource bundle
     */
    //public abstract List<String> getComponentLabels(Locale locale);
    //public String getComponentLabel(Locale locale);
}