![JAR search and dependency download from the Maven repository](/logo.png)
com.imsweb.layout.record.fixed.FixedColumnsLayout Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of layout Show documentation
Show all versions of layout Show documentation
Framework that allows defining file formats (layouts) and use them to read and write data files.
/*
* Copyright (C) 2011 Information Management Services, Inc.
*/
package com.imsweb.layout.record.fixed;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.imsweb.layout.Field;
import com.imsweb.layout.Field.FieldAlignment;
import com.imsweb.layout.LayoutFactory;
import com.imsweb.layout.LayoutInfo;
import com.imsweb.layout.LayoutInfoDiscoveryOptions;
import com.imsweb.layout.LayoutUtils;
import com.imsweb.layout.record.RecordLayout;
import com.imsweb.layout.record.RecordLayoutOptions;
import com.imsweb.layout.record.fixed.xml.FixedColumnLayoutFieldXmlDto;
import com.imsweb.layout.record.fixed.xml.FixedColumnLayoutXmlDto;
/**
* This class contains the logic related to fixed-columns layouts.
*
* Created on Jul 28, 2011 by depryf
* @author depryf
*/
public class FixedColumnsLayout extends RecordLayout {
/**
* Line length
*/
protected Integer _layoutLineLength;
/**
* The fields for this layout
*/
protected List _fields = new ArrayList<>();
/**
* Cached fields by name
*/
protected Map _cachedByName = new HashMap<>();
/**
* Cached fields by NAACCR Item Number
*/
protected Map _cachedByNaaccrItemNumber = new HashMap<>();
/**
* Default constructor.
*/
public FixedColumnsLayout() {
super();
}
/**
* Constructor.
* @param layoutUrl URL to the XML definition, cannot be null
* @throws IOException if the XML definition is not valid
*/
public FixedColumnsLayout(URL layoutUrl) throws IOException {
this();
if (layoutUrl == null)
throw new NullPointerException("Unable to create layout from null URL");
try (InputStream is = layoutUrl.openStream()) {
init(LayoutUtils.readFixedColumnsLayout(is), false);
}
}
/**
* Constructor.
* @param layoutFile XML definition, cannot be null, must exist
* @throws IOException if the XML definition is not valid
*/
public FixedColumnsLayout(File layoutFile) throws IOException {
this();
if (layoutFile == null)
throw new NullPointerException("Unable to create layout from null file");
if (!layoutFile.exists())
throw new IOException("Unable to read from " + layoutFile.getPath());
try (InputStream is = Files.newInputStream(layoutFile.toPath())) {
init(LayoutUtils.readFixedColumnsLayout(is), false);
}
}
/**
* Constructor.
* @param layoutXmlDto java representation of the XML definition, cannot be null
* @throws IOException if the XML definition is not valid
*/
public FixedColumnsLayout(FixedColumnLayoutXmlDto layoutXmlDto) throws IOException {
this();
if (layoutXmlDto == null)
throw new NullPointerException("Unable to create layout from null XML object");
init(layoutXmlDto, false);
}
protected void init(FixedColumnLayoutXmlDto layoutXmlDto, boolean useDeprecatedFieldNames) throws IOException {
_layoutId = layoutXmlDto.getId();
_layoutName = layoutXmlDto.getName();
_layoutVersion = layoutXmlDto.getVersion();
_layoutDesc = layoutXmlDto.getDescription();
_layoutLineLength = layoutXmlDto.getLength();
// are we extending another layout?
FixedColumnsLayout parentLayout = null;
if (layoutXmlDto.getExtendLayout() != null) {
if (!LayoutFactory.getAvailableLayouts().containsKey(layoutXmlDto.getExtendLayout()))
throw new IOException("Unable to find referenced layout ID '" + layoutXmlDto.getExtendLayout() + "'");
try {
parentLayout = (FixedColumnsLayout)LayoutFactory.getLayout(layoutXmlDto.getExtendLayout());
}
catch (ClassCastException e) {
throw new IOException("A fixed-columns layout must extend another fixed-columns layout");
}
_parentLayoutId = parentLayout.getLayoutId();
}
_fields.clear();
if (layoutXmlDto.getField() != null)
for (FixedColumnLayoutFieldXmlDto fieldXmlDto : layoutXmlDto.getField())
addField(createFieldFromXmlField(fieldXmlDto, useDeprecatedFieldNames));
if (parentLayout != null)
for (FixedColumnsField field : parentLayout.getAllFields())
if (!_cachedByName.containsKey(field.getName()))
addField(field);
// sort the fields by start column
_fields.sort(Comparator.comparing(FixedColumnsField::getStart));
// final verifications
try {
verify();
}
catch (Exception e) {
throw new IOException(e.getMessage());
}
}
protected String getDeprecatedFieldName(String name) {
return name;
}
/**
* Helper that translates an XML field into an exposed DTO field.
*
* Created on Aug 16, 2011 by depryf
* @param dto FixedColumnLayoutFieldXmlDto
to translate
* @param useDeprecatedFieldNames if true, the XML field name will be translated to use an old version of the name (only applicable to NAACCR layouts)
* @return the translated Field
*/
protected FixedColumnsField createFieldFromXmlField(FixedColumnLayoutFieldXmlDto dto, boolean useDeprecatedFieldNames) throws IOException {
FixedColumnsField field = new FixedColumnsField();
field.setName(useDeprecatedFieldNames ? getDeprecatedFieldName(dto.getName()) : dto.getName());
field.setLongLabel(dto.getLongLabel());
field.setShortLabel(dto.getShortLabel() != null ? dto.getShortLabel() : dto.getLongLabel());
field.setNaaccrItemNum(dto.getNaaccrItemNum());
if (dto.getStart() == null)
throw new IOException("Start column is required");
field.setStart(dto.getStart());
if (dto.getEnd() == null)
throw new IOException("End column is required");
field.setEnd(dto.getEnd());
field.setLength(dto.getEnd() - dto.getStart() + 1);
if (dto.getStart() > dto.getEnd())
throw new IOException("Field " + field.getName() + " defines a end column that is greater than its start column");
if (dto.getAlign() != null) {
try {
field.setAlign(FieldAlignment.fromValue(dto.getAlign()));
}
catch (IllegalArgumentException e) {
throw new IOException("Field " + field.getName() + " defines an invalid align option: " + dto.getAlign());
}
}
else
field.setAlign(FieldAlignment.LEFT);
field.setPadChar(dto.getPadChar() == null ? " " : dto.getPadChar());
field.setDefaultValue(dto.getDefaultValue());
field.setTrim(dto.getTrim() == null ? Boolean.TRUE : dto.getTrim());
if (dto.getField() != null && !dto.getField().isEmpty()) {
List subFields = new ArrayList<>();
for (FixedColumnLayoutFieldXmlDto childDto : dto.getField())
subFields.add(createFieldFromXmlField(childDto, useDeprecatedFieldNames));
field.setSubFields(subFields);
}
field.setSection(dto.getSection());
return field;
}
private void addField(FixedColumnsField field) {
// update collection of fields
_fields.add(field);
// update name cache
_cachedByName.put(field.getName(), field);
if (field.getSubFields() != null)
for (FixedColumnsField f : field.getSubFields())
_cachedByName.put(f.getName(), f);
// update NAACCR cache
if (field.getNaaccrItemNum() != null)
_cachedByNaaccrItemNumber.put(field.getNaaccrItemNum(), field);
if (field.getSubFields() != null)
for (FixedColumnsField f : field.getSubFields())
if (f.getNaaccrItemNum() != null)
_cachedByNaaccrItemNumber.put(f.getNaaccrItemNum(), f);
}
public void setFields(Collection fields) {
_fields.clear();
fields.forEach(this::addField);
// sort the fields by start column
_fields.sort(Comparator.comparing(FixedColumnsField::getStart));
// verify they make sense
verify();
}
/**
* Getter for layout line length.
*
* Created on Jun 25, 2012 by depryf
* @return the layout line length
*/
public Integer getLayoutLineLength() {
return _layoutLineLength;
}
/**
* Setter for layout line length.
*
* Created on Jun 25, 2012 by depryf
* @param lineLength length
*/
public void setLayoutLineLength(Integer lineLength) {
_layoutLineLength = lineLength;
}
@Override
public FixedColumnsField getFieldByName(String name) {
return _cachedByName.get(name);
}
@Override
public FixedColumnsField getFieldByNaaccrItemNumber(Integer num) {
return _cachedByNaaccrItemNumber.get(num);
}
@Override
public List getAllFields() {
return Collections.unmodifiableList(_fields);
}
@Override
public String validateLine(String line, Integer lineNumber) {
// if this layout extends another one, delegate to the other one...
if (_parentLayoutId != null)
return ((FixedColumnsLayout)LayoutFactory.getLayout(_parentLayoutId)).validateLine(line, lineNumber);
StringBuilder msg = new StringBuilder();
if (line == null || line.isEmpty())
msg.append("line ").append(lineNumber).append(": line is empty");
else if (line.length() != _layoutLineLength)
msg.append("line ").append(lineNumber).append(": wrong length, expected ").append(_layoutLineLength).append(" but got ").append(line.length());
return msg.length() == 0 ? null : msg.toString();
}
@Override
@SuppressWarnings("java:S3776") // logic too complicated
public String createLineFromRecord(Map rec, RecordLayoutOptions options) throws IOException {
StringBuilder result = new StringBuilder();
if (rec == null)
rec = new HashMap<>();
int currentIndex = 1;
for (FixedColumnsField field : _fields) {
int start = field.getStart();
int end = field.getEnd();
// adjust for the "leading" gap
if (start > currentIndex)
for (int i = 0; i < start - currentIndex; i++)
result.append(' ');
currentIndex = start;
// get value; if the field defines subfields, always use the subfields (#162)
if (field.getSubFields() != null && !field.getSubFields().isEmpty()) {
for (FixedColumnsField child : field.getSubFields()) {
int subStart = child.getStart();
int subEnd = child.getEnd();
// adjust for the "leading" gap within the subfields
if (subStart > currentIndex)
for (int i = 0; i < subStart - currentIndex; i++)
result.append(' ');
currentIndex = subStart;
if (subEnd <= end) { // do not write the current subfield out if it can potentially go out of the field
String value = rec.get(child.getName());
if (value == null)
value = child.getDefaultValue() != null ? child.getDefaultValue() : "";
int length = subEnd - subStart + 1;
if (value.length() > length) {
if (options != null && RecordLayoutOptions.VAL_TOO_LONG_NULLIFY.equals(options.getValueTooLongHandling()))
value = "";
else if (options != null && RecordLayoutOptions.VAL_TOO_LONG_CUTOFF.equals(options.getValueTooLongHandling()))
value = value.substring(0, length);
else
throw new IOException("value too long for field '" + field.getName() + "'");
}
String paddingChar = !value.isEmpty() && applyPadding(options) ? child.getPadChar() : " ";
if (applyAlignment(options) && child.getAlign() == FieldAlignment.RIGHT)
value = LayoutUtils.pad(value, length, paddingChar, true);
else
value = LayoutUtils.pad(value, length, paddingChar, false);
result.append(cleanValue(value, child));
currentIndex = subEnd + 1;
}
}
// adjust for the "trailing" gap within the subfields
if (currentIndex <= end)
for (int i = 0; i < end - currentIndex + 1; i++)
result.append(' ');
currentIndex = end + 1;
}
else if (end <= _layoutLineLength) { // do not write the current field out if it can potentially go out of the line
String value = rec.get(field.getName());
if (value == null)
value = field.getDefaultValue() != null ? field.getDefaultValue() : "";
int length = end - start + 1;
if (value.length() > length) {
if (options != null && RecordLayoutOptions.VAL_TOO_LONG_NULLIFY.equals(options.getValueTooLongHandling()))
value = "";
else if (options != null && RecordLayoutOptions.VAL_TOO_LONG_CUTOFF.equals(options.getValueTooLongHandling()))
value = value.substring(0, length);
else
throw new IOException("value too long for field '" + field.getName() + "'");
}
String paddingChar = !value.isEmpty() && applyPadding(options) ? field.getPadChar() : " ";
if (applyAlignment(options) && field.getAlign() == FieldAlignment.RIGHT)
value = LayoutUtils.pad(value, length, paddingChar, true);
else
value = LayoutUtils.pad(value, length, paddingChar, false);
result.append(cleanValue(value, field));
currentIndex = end + 1;
}
}
// adjust for the "leading" gap
if (currentIndex <= _layoutLineLength)
for (int i = 0; i < _layoutLineLength - currentIndex + 1; i++)
result.append(' ');
return result.toString();
}
/**
* Clean the value for the given field; this default implementation does nothing but it delegates to the parent if this is an extending layout.
*/
protected String cleanValue(String value, Field field) {
// if this layout extends another one, delegate to the other one...
if (_parentLayoutId != null)
return ((FixedColumnsLayout)LayoutFactory.getLayout(_parentLayoutId)).cleanValue(value, field);
return value;
}
@Override
@SuppressWarnings({"StringOperationCanBeSimplified", "java:S2129"}) // creating new String instances
public Map createRecordFromLine(String line, Integer lineNumber, RecordLayoutOptions options) throws IOException {
Map result = new HashMap<>();
Integer lineNumberSafe = lineNumber == null ? Integer.valueOf(1) : lineNumber;
// handle special case
if (line == null || line.isEmpty()) {
if (enforceStrictFormat(options))
throw new IOException("line " + lineNumberSafe + ": got en empty line");
else
return result;
}
// if we need to enforce the format, get the expected line length; otherwise read until the EOL
if (enforceStrictFormat(options)) {
String validationMsg = validateLine(line, lineNumberSafe);
if (validationMsg != null)
throw new IOException(validationMsg);
}
for (FixedColumnsField field : _fields) {
int start = field.getStart();
int end = field.getEnd();
if (end > line.length())
break;
// http://blog.mikemccandless.com/2010/06/beware-stringsubstrings-memory-usage.html
String value = new String(line.substring(start - 1, end));
// can we trim the value?
boolean childPreventsTrimming = false;
if (field.getSubFields() != null)
childPreventsTrimming = field.getSubFields().stream().anyMatch(subField -> !subField.getTrim());
String trimmedValue = value.trim();
if (trimValues(options) && (field.getTrim() || (trimmedValue.isEmpty() && !childPreventsTrimming))) {
// never trim a group field unless it's completely empty (or we would lose the info of which child value is which)
if (field.getSubFields() == null || trimmedValue.isEmpty())
value = trimmedValue;
}
if (!value.isEmpty()) {
result.put(field.getName(), value);
// handle children fields if any
if (field.getSubFields() != null) {
for (FixedColumnsField child : field.getSubFields()) {
start = child.getStart();
end = child.getEnd();
value = new String(line.substring(start - 1, end));
if (trimValues(options) && Boolean.TRUE.equals(child.getTrim()))
value = value.trim();
if (!value.isEmpty()) {
result.put(child.getName(), value);
}
}
}
}
}
return result;
}
@Override
public LayoutInfo buildFileInfo(String firstRecord, LayoutInfoDiscoveryOptions options) {
LayoutInfo result = null;
// this default implementation is based only on the line length, only if we don't enforce the strict format
if (firstRecord != null && options.isFixedColumnAllowDiscoveryFromLineLength() && firstRecord.length() == _layoutLineLength) {
result = new LayoutInfo();
result.setLayoutId(getLayoutId());
result.setLayoutName(getLayoutName());
result.setLineLength(_layoutLineLength);
}
return result;
}
/**
* Verify the internal fields, throws a runtime exception if something is wrong.
*
* Created on Jun 25, 2012 by depryf
*/
public void verify() {
// ID is required
if (_layoutId == null)
throw new IllegalStateException("Layout ID is required");
// name is required
if (_layoutName == null)
throw new IllegalStateException("Layout name is required");
// line length is required
if (_layoutLineLength == null)
throw new IllegalStateException("Line length is required");
if (!_fields.isEmpty()) {
// verify first field starts at 1 or greater
if (_fields.get(0).getStart() <= 0)
throw new IllegalStateException("First field start column must be greater or equals to 1");
// verify last field ends at line length or smaller
if (_fields.get(_fields.size() - 1).getEnd() > _layoutLineLength)
throw new IllegalStateException("Last field end column must be smaller or equals to defined line length");
// verify each field
for (int i = 0; i < _fields.size() - 1; i++) {
FixedColumnsField f1 = _fields.get(i);
// verify field start is within the allowed range
if (f1.getStart() <= 0 || f1.getStart() > _layoutLineLength)
throw new IllegalStateException("Field " + f1.getName() + " start value is invalid, must be within 1-" + _layoutLineLength + " but got " + f1.getStart());
// verify field end is within the allowed range
if (f1.getEnd() <= 0 || f1.getEnd() > _layoutLineLength)
throw new IllegalStateException("Field " + f1.getName() + " end value is invalid, must be within 1-" + _layoutLineLength + " but got " + f1.getStart());
// verify there is no overlapping
FixedColumnsField f2 = _fields.get(i + 1);
if (f1.getEnd() >= f2.getStart())
throw new IllegalStateException("Fields " + f1.getName() + " and " + f2.getName() + " are overlapping");
// also verify the subfields, only need to do this on f1
if (f1.getSubFields() != null) {
List list = f1.getSubFields();
int size = list.size();
for (int j = 0; j < size; j++) {
FixedColumnsField ff = list.get(j);
if (j == 0 && ff.getStart() < f1.getStart())
throw new IllegalStateException("Field " + f1.getName() + " defines a sub-field that starts before it");
if (j == size - 1 && ff.getEnd() > f1.getEnd())
throw new IllegalStateException("Field " + f1.getName() + " defines a sub-field that ends after it");
if (j < size - 1 && ff.getEnd() >= list.get(j + 1).getStart())
throw new IllegalStateException("Field " + f1.getName() + " defines overlapping subfields");
}
}
}
Set names = new HashSet<>();
Set naaccrItemNums = new HashSet<>();
for (FixedColumnsField field : _fields) {
if (field.getName() == null)
throw new IllegalStateException("Field name is required");
if (names.contains(field.getName()))
throw new IllegalStateException("Field name must be unique, found duplicate name for '" + field.getName() + "'");
names.add(field.getName());
if (field.getNaaccrItemNum() != null) {
if (naaccrItemNums.contains(field.getNaaccrItemNum().toString()))
throw new IllegalStateException("Field NAACCR item number must be unique, found duplicate number for '" + field.getNaaccrItemNum() + "'");
naaccrItemNums.add(field.getNaaccrItemNum().toString());
}
if (field.getSubFields() != null) {
for (Field f : field.getSubFields()) {
if (f.getName() == null)
throw new IllegalStateException("Field name is required");
if (names.contains(f.getName()))
throw new IllegalStateException("Field name must be unique, found duplicate name for '" + f.getName() + "'");
names.add(f.getName());
if (f.getNaaccrItemNum() != null) {
if (naaccrItemNums.contains(f.getNaaccrItemNum().toString()))
throw new IllegalStateException("Field NAACCR item number must be unique, found duplicate number for '" + f.getNaaccrItemNum() + "'");
naaccrItemNums.add(f.getNaaccrItemNum().toString());
}
}
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy