com.prowidesoftware.swift.model.field.Field Maven / Gradle / Ivy
Show all versions of pw-swift-core Show documentation
/*
* Copyright 2006-2023 Prowide
*
* 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.prowidesoftware.swift.model.field;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.prowidesoftware.JsonSerializable;
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;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateFormatUtils;
/**
* Base class implemented by classes that provide general access to field components.
*
* @author sebastian
* @since 6.0
*/
public abstract class Field implements PatternContainer, JsonSerializable {
private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(Field.class.getName());
/**
* Zero based list of field components in String format.
* For example: for field content ":FOO//EUR1234 will be components[0]=FOO, components[1]=EUR and components[1]=1234
*/
protected List components;
// cached results for getLabelMap() method
protected Map labelMap;
private static final String fieldNamePattern = "^\\d{2,3}[A-Z]?$";
/**
* No-argument constructor required for serialization.
* Initializes the components list.
*/
protected Field() {
this.components = new ArrayList<>();
}
/**
* Creates a field with the list of components initialized to the given number of components.
*
* @param components the number of components to initialize
* @see #init(int)
*/
protected Field(final int components) {
init(components);
}
/**
* 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) {
parse(value);
/*
* trim empty components to null
*/
for (int i = 0; i < this.components.size(); i++) {
if (StringUtils.isEmpty(this.components.get(i))) {
this.components.set(i, null);
}
}
}
/**
* Copy constructor.
* 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<>(source.getComponents());
}
/**
* Format the given object as a money number without currency information in format
*
* @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());
}
return fmt.format(aValue);
}
/**
* @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) {
if (d != null) {
try {
return DateFormatUtils.format(d.getTime(), "dd/MM/yyyy");
} catch (final Exception e) {
log.log(Level.WARNING, "error formatting date", e);
}
}
return StringUtils.EMPTY;
}
/**
* A formatted account with a fixed format nnnn-nnnnn-nnn-n
*
* @param a string with an account number or null
* @return the formatted account or an empty String if param is null
*/
// TODO support user formatting masks from property file
protected static String formatAccount(final String a) {
if (a != null && a.length() > 12) {
final StringBuilder result = new StringBuilder(a);
try {
result.insert(4, '-');
result.insert(9, '-');
result.insert(12, '-');
return result.toString();
} catch (final Exception e) {
log.log(Level.WARNING, "error formatting account", e);
}
}
return StringUtils.EMPTY;
}
/**
* Gets a BigDecimal from a generic Number argument
*
* @return BigDecimal value of number parameter
*/
public static BigDecimal getAsBigDecimal(final Number number) {
if (number instanceof BigDecimal) {
return (BigDecimal) number;
} else if (number instanceof Long) {
return new BigDecimal(number.longValue());
} else if (number instanceof Integer) {
return new BigDecimal(number.intValue());
} else if (number instanceof Short) {
return new BigDecimal(number.intValue());
} else if (number instanceof Double) {
return new BigDecimal(number.toString());
} else {
throw new IllegalArgumentException("class " + number.getClass().getName() + " is not supported");
}
}
/**
* 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 null if exceptions occur during object creation.
*/
public static 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 null to create the field with no initial content
* @return a specific field object (example: Field32A) or null if exceptions occur during object creation.
* @since 7.8
*/
public static 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 = {String.class};
@SuppressWarnings("rawtypes")
final Constructor ct = c.getConstructor(argsClass);
final Object[] arglist = {value};
r = ct.newInstance(arglist);
} catch (final ClassNotFoundException e) {
log.warning(
"Field class for Field" + name
+ " not found. This is normally caused by an unrecognized field in the message or a malformed message block structure.");
} catch (final Exception e) {
log.log(Level.WARNING, "An error occurred while creating an instance of " + name, e);
}
return (Field) r;
}
/**
* Same as {@link #getLabel(String, String, String, Locale)} using default locale
*
* @since 7.8
*/
public static 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:
*
* - 50K[103][B]
* - 50a[103][B]
* - 50K[103]
* - 50a[103]
* - 50K
* - 50a
* - 50
*
*
* @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 null.
* @param sequence optional indication of sequence or null 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
*/
public static 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
*/
public static 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");
}
private static 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
*
* @return the found resource or null 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 ignored) {
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 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;
}
// 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 name.length() != 3
|| Character.isDigit(name.charAt(2))
|| name.charAt(2) == 'a'
|| Character.isUpperCase(name.charAt(2));
}
/**
* Creates a specific field instance from its JSON representation.
*
* The implementation reads the "name" property in the JSON data, then calls the fromJson method in the specific
* Field subclass
*
* @return a specific field, for example Field32A, or null if the JSON data is not well-formed or contains an unrecognized field name
* @see #toJson()
*/
public static Field fromJson(final String json) {
JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
JsonElement nameElement = jsonObject.get("name");
if (nameElement != null) {
String name = nameElement.getAsString();
if (StringUtils.isBlank(name)) {
log.warning("Field name is empty");
return null;
}
if (!name.matches(fieldNamePattern)) {
log.warning(
"Invalid field name [" + name
+ "]. The field name must start with 2 or 3 numeric digits and can optionally end with a single capital letter.");
return null;
}
try {
final Class> c = Class.forName("com.prowidesoftware.swift.model.field.Field" + name);
Method method = c.getMethod("fromJson", String.class);
return (Field) method.invoke(null, json);
} catch (final ClassNotFoundException e) {
log.warning(
"Field class for Field" + name
+ " not found. This is normally caused by an unrecognized field in the message or a malformed message block structure.");
} catch (final Exception e) {
log.log(Level.WARNING, "An error occured while creating an instance of " + name, e);
}
return null;
}
return null;
}
/**
* Initialize the list of components to the indicated size and sets all values to null
*
* @param components the number of components to initialize
* @since 7.8
*/
protected void init(final int components) {
this.components = new ArrayList<>(components);
for (int i = 0; i < components; i++) {
this.components.add(null);
}
}
/**
* 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.
*
* Implemented by subclasses with logic for each specific field structure.
*
* @param value complete field value including separators and CRLF
* @since 7.8
*/
public abstract void parse(final String value);
/**
* Implementation of toString using ToStringBuilder from commons-lang
*/
@Override
public String toString() {
return org.apache.commons.lang3.builder.ToStringBuilder.reflectionToString(this);
}
/**
* Implementation of equals using EqualsBuilder from commons-lang
*/
@Override
public boolean equals(final Object obj) {
return org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals(this, obj);
}
/**
* Implementation of hashCode using HashCodeBuilder from commons-lang
*/
@Override
public int hashCode() {
return org.apache.commons.lang3.builder.HashCodeBuilder.reflectionHashCode(this);
}
/**
* 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) {
Objects.requireNonNull(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 || 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) {
Objects.requireNonNull(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 getComponents() {
return components;
}
/**
* @param components list to set
*/
public void setComponents(final List 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, "components are numerated starting at 1, cannot insert a component with number " + number);
// internal position index is zero based
final int position = number - 1;
if (this.components == null) {
this.components = new ArrayList<>();
}
if (position >= this.components.size()) {
log.warning("component number " + number + " is out of bound for field " + getName());
} else {
this.components.set(position, value);
}
}
/**
* 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 null
*/
public String getComponent(final int number) {
// internal position index is zero based
final int position = number - 1;
if (this.components != null && position >= 0 && position < this.components.size()) {
return this.components.get(position);
}
return null;
}
/**
* Inserts a component String value into the list of components, using the component name to position the value into the List.
*
* @param componentName name of component to set
* @param value String value of the parsed component (without component separators ':', '/', '//')
* @since 9.3.12
*/
public void setComponent(final String componentName, final String value) {
int componentNameToNumber = componentNameToNumber(componentName);
if (componentNameToNumber > 0) {
setComponent(componentNameToNumber, value);
} else {
log.warning("Component Name: '" + componentName + "' is not part of field " + getName());
}
}
/**
* Get the component number based on the component name
*
* @param componentName name of component to get
* @return the component number or zero if the component name is not part of the field
* @since 9.3.12
*/
public int componentNameToNumber(final String componentName) {
Validate.isTrue(StringUtils.isNotBlank(componentName), "component name should not be empty " + componentName);
Integer component = getLabelMap().get(componentName.toLowerCase());
if (component != null) {
return component;
}
return 0;
}
/**
* Gets a specific component (subfield) value given its name.
*
* @param componentName name of component to get
* @return found component or null if the component name is not part of the field or the value for the component is not set
* @since 9.3.12
*/
public String getComponent(final String componentName) {
int componentNumber = componentNameToNumber(componentName);
return componentNumber > 0 ? this.getComponent(componentNumber) : 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);
/**
* Returns a localized suitable for showing to humans string of a field component.
*
* @param componentName name 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 name is invalid or not present
* @throws IllegalArgumentException if component name is invalid for the field
* @since 9.3.12
*/
public String getValueDisplay(String componentName, Locale locale) {
int componentNumber = componentNameToNumber(componentName);
return componentNumber > 0 ? this.getValueDisplay(componentNumber, locale) : null;
}
/**
* 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 componentName name of the component to retrieve
* @throws IllegalArgumentException if c is not any of: String, BIC, Currency, Number, BigDecimal Character or Integer
* @see #getComponent(int)
* @since 9.3.12
*/
public Object getComponentAs(final String componentName, @SuppressWarnings("rawtypes") final Class c) {
int componentNumber = componentNameToNumber(componentName);
return componentNumber > 0 ? this.getComponentAs(componentNumber, c) : null;
}
/**
* 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
* @throws IllegalArgumentException if c is not any of: String, BIC, Currency, Number, BigDecimal Character or Integer
* @see #getComponent(int)
*/
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)) {
return SwiftFormatUtils.getNumber(s);
} else if (c.equals(BigDecimal.class)) {
return SwiftFormatUtils.getBigDecimal(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 if (c.equals(Long.class)) {
return Long.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 internal y calls {@link #getComponentAsNumber(int)}, and casts the result
*
* @since 9.3.12
*/
public Object getComponentAsNumber(final String componentName) {
int componentNumber = componentNameToNumber(componentName);
return componentNumber > 0 ? getComponentAsNumber(componentNumber) : 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 toAdd = new ArrayList<>();
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.
*
* @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
*
* @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);
}
/**
* Returns the first component starting with the given prefix value or null if not found.
*
* @return s
*/
public String findComponentStartingWith(final String prefix) {
for (final String c : this.components) {
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
* /ACC/BLABLABLA CrLf
* //BLABLABLA CrLf
* /INS/CITIUS33MIA CrLf
* //BLABLABLA CrLf
* A call to this method with parameter "INS" will return "CITIUS33MIA"
*
* @return the found value or null if not found
* @see #findComponentStartingWith(String)
*/
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 components into the a plain string value in SWIFT format.
*
* This method implementation is specific for each field. All not null
* components are appended to the result string with proper components
* separators like ':', slashes and CRLF.
*
*
For any valid field this is always true:
* new Field(v)).getValue() = v
* meaning plain value integrity must be preserved after parsing the value
* into components and serializing it back into the plain value.
* Conversely this may not be true when the parsed field value is invalid
* because the parser will do a best effort to gather as many valid components
* as possible and the serialization will also do a best effort to generate
* valid content.
*
* @return SWIFT formatted value
*/
public abstract String getValue();
/**
* 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 (StringUtils.isNotBlank(c)) {
return false;
}
}
return true;
}
/**
* 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 component types pattern
*
* This method returns a letter representing the type for each component in the Field. It supersedes
* the Components Pattern because it distinguishes between N (Number) and I (BigDecimal).
*/
@Override
public abstract String typesPattern();
/**
* Returns the field's validator pattern
*
* @since 7.8
*/
public abstract String validatorPattern();
public abstract boolean isOptional(int component);
public abstract boolean isGeneric();
/**
* Return the letter option of this field as given by it classname or null 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 == c;
}
return false;
}
/**
* Compares the this fields's name with a list of names to check
*
* @param names must not be null nor empty
* @return true if this field names equals one in the list of names and false otherwise
* @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;
}
/**
* Compares this field component 1 with the parameter value
*
* Same as is(1, compare)
*
* If the field has only one component this is the same as comparing against field value
*
* @param compare string to compare
* @return true if the first component is equal to the parameter
*/
public boolean is(final String compare) {
return StringUtils.equals(compare, getComponent(1));
}
/**
* Compares a specific component with the parameter value
*
* @param componentNumber component number 1-based
* @param compare string to compare
* @return true if the indicated component value is equal to the parameter
*/
public boolean is(final int componentNumber, final String compare) {
return StringUtils.equals(compare, getComponent(componentNumber));
}
/**
* Compares this field components 1 and 2 with the parameter values.
*
* @param compare1 string to compare with component 1
* @param compare2 string to compare with component 2
* @return true if components 1 and 2 are equal the parameter values respectively
*/
public boolean is(final String compare1, final String compare2) {
return StringUtils.equals(compare1, getComponent(1)) && StringUtils.equals(compare2, getComponent(2));
}
/**
* Compares this field component 1 with the parameter values.
*
* If the field has only one component this is the same as comparing against the field value
*
* @param values the values to compare
* @return true if the first component is equal to any of the given values
* @since 7.9.7
*/
public boolean is(final String... values) {
final String comp1 = getComponent(1);
if (values != null) {
for (String value : values) {
if (StringUtils.equals(comp1, value)) {
return true;
}
}
}
return false;
}
/**
* Get the generic tag object of this field.
*/
public Tag asTag() {
return new Tag(getName(), getValue());
}
/**
* Returns the defined amount of components.
* 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.
*
* Also notice that a line may be composed by several components, there is
* no linear relation between component numbers and lines numbers.
*
* @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 lines = SwiftParseUtils.getLines(cp.getValue()).stream()
.filter(StringUtils::isNotEmpty)
.filter(l -> !onlySlashes(l))
.collect(Collectors.toList());
if (start != null) {
if (lines.size() >= start) {
if (end != null) {
if (end >= start) {
int trimmedEnd = end;
if (end > lines.size()) {
trimmedEnd = lines.size() - 1;
}
// return line subset
return asString(hash, lines.subList(start - 1, trimmedEnd));
} 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));
}
}
} else {
// return all lines from offset
return asString(hash, lines);
}
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
* @return a string with the final, cleaned, joined lines
*/
private String asString(final String hash, final List list) {
final StringBuilder result = new StringBuilder();
for (final String l : list) {
final String trimmed = clean(hash, l);
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.
*
* 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
* @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) {
// try to replace /hash first just in case the component is optional then replace the hash only if present
String trimmed = StringUtils.replace(StringUtils.replace(value, "/" + hash, ""), hash, "");
if (StringUtils.isNotBlank(trimmed) && !onlySlashes(trimmed)) {
/*
* sebastian Oct 2015
* We should remove meaningless component separators separators (':' and '/') from the result depending on
* the offset, removing it only if the offset is in the middle of a line (more than one component are in
* the same field line and the offset parameter to the getLine is not for the first component in the line).
* We cannot do that at the moment because there is no relation in the model to match components and lines.
* We just do a specific patch below for specific cases based on the test cases we have.
*/
if (trimmed.startsWith("://")) {
return StringUtils.substringAfter(trimmed, "://");
} else {
return trimmed;
}
}
// otherwise return null
return null;
}
/**
* Returns english label for components.
*
* The index in the list is in sync with specific field component structure.
*
* @see #getComponentLabel(int)
* @since 7.8.4
*/
public abstract List getComponentLabels();
/**
* Returns english label for the component.
*
*
* @param componentName name of the component to get
* @return found label or null if it is not defined
* @since 9.3.12
*/
public String getComponentLabel(final String componentName) {
int componentNumber = componentNameToNumber(componentName);
return componentNumber > 0 ? getComponentLabel(componentNumber) : null;
}
/**
* Returns english label for the component.
*
*
* @param number one-based index of component, first component of a field should be number one
* @return found label or null 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 labels = getComponentLabels();
if (labels != null && position >= 0 && position < labels.size()) {
return labels.get(position);
}
return null;
}
/**
* Returns a mapping between component numbers and their label in camel case format.
*
* @since 7.10.3
*/
protected abstract Map getComponentMap();
/**
* Returns a mapping between component labels and the internal component number.
* Component labels are in lowercase and without spaces and separators.
* If a for a given component there is more than one label option, both are mapped to the same component number.
*
* @since 9.3.12
*/
protected abstract Map getLabelMap();
/**
* Returns english label for the component in camel case format.
*
*
* @param number one-based index of component, first component of a field should be number one
* @return found label or null
if it is not defined
* @since 7.10.3
*/
private String getComponentLabelCamelCase(final int number) {
final Map labels = getComponentMap();
if (labels != null && number >= 0) {
return labels.get(number);
}
return null;
}
/**
* Ensures a not-null locale parameter.
*
* @param locale a locale or null
* @return the parameter locale if it is not null or the default locale
* @since 7.8.8
*/
protected final Locale notNull(final Locale locale) {
if (locale != null) {
return locale;
} else {
return Locale.getDefault();
}
}
/*
* 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 getComponentLabels(Locale locale);
// public String getComponentLabel(Locale locale);
/**
* Appends a not null field component to the builder.
*
* This helper method is used by subclasses implementation of {@link #getValue()}
*
* @param result string where component content is appended
* @param component component number
* @since 7.9.3
*/
protected void append(StringBuilder result, int component) {
final String value = getComponent(component);
if (value != null && result != null) {
result.append(value);
}
}
/**
* Get a json representation of this message with expanded fields content.
*
* The JSON representation for fields contains the field name and the components with camel case labels, for example:
*
{"name":"32A","date":"010203","currency":"USD","amount":"123"}
*
* @since 7.10.3
*/
@Override
public String toJson() {
String stdFormat = toJsonFormat();
if (this instanceof StructuredNarrativeField) {
return stdFormat.substring(0, stdFormat.length() - 1) + ","
+ toNarrativeFormat().substring(1);
}
return stdFormat;
}
String toJsonFormat() {
JsonObject field = new JsonObject();
field.addProperty("name", this.getName());
for (int i = 1; i <= this.getComponents().size(); i++) {
if (this.getComponent(i) != null) {
String label = this.getComponentLabelCamelCase(i);
if (label == null) {
label = "value";
}
field.addProperty(label, this.getComponent(i));
}
}
return field.toString();
}
String toNarrativeFormat() {
Narrative narrative = ((StructuredNarrativeField) this).narrative();
Gson gson = new Gson();
return gson.toJson(narrative);
}
}