/*
* Milyn - Copyright (C) 2006 - 2010
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License (version 2.1) as published by the Free Software
* Foundation.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* See the GNU Lesser General Public License for more details:
* http://www.gnu.org/licenses/lgpl.txt
*/
package org.milyn.flatfile.variablefield;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.milyn.SmooksException;
import org.milyn.cdr.SmooksConfigurationException;
import org.milyn.cdr.SmooksResourceConfiguration;
import org.milyn.cdr.annotation.AnnotationConstants;
import org.milyn.cdr.annotation.ConfigParam;
import org.milyn.container.ExecutionContext;
import org.milyn.delivery.VisitorAppender;
import org.milyn.delivery.VisitorConfigMap;
import org.milyn.delivery.annotation.Initialize;
import org.milyn.delivery.dom.DOMVisitAfter;
import org.milyn.delivery.ordering.Consumer;
import org.milyn.delivery.sax.SAXElement;
import org.milyn.delivery.sax.SAXVisitAfter;
import org.milyn.expression.MVELExpressionEvaluator;
import org.milyn.flatfile.BindingType;
import org.milyn.flatfile.FieldMetaData;
import org.milyn.flatfile.RecordMetaData;
import org.milyn.flatfile.RecordParserFactory;
import org.milyn.javabean.Bean;
import org.milyn.javabean.context.BeanContext;
import org.milyn.xml.XmlUtil;
import org.w3c.dom.Element;
/**
* Abstract VariableFieldRecordParserFactory.
*
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
*/
public abstract class VariableFieldRecordParserFactory implements RecordParserFactory, VisitorAppender {
@ConfigParam(defaultVal = AnnotationConstants.NULL_STRING)
private String fields;
private VariableFieldRecordMetaData vfRecordMetaData;
@ConfigParam(use = ConfigParam.Use.OPTIONAL)
private String recordDelimiter;
private Pattern recordDelimiterPattern;
@ConfigParam(defaultVal = "false")
private boolean keepDelimiter;
@ConfigParam(defaultVal = "record")
private String recordElementName;
@ConfigParam(use = ConfigParam.Use.OPTIONAL)
private String bindBeanId;
@ConfigParam(use = ConfigParam.Use.OPTIONAL)
private Class<?> bindBeanClass;
@ConfigParam(use = ConfigParam.Use.OPTIONAL)
private BindingType bindingType;
@ConfigParam(use = ConfigParam.Use.OPTIONAL)
private String bindMapKeyField;
private static final String RECORD_BEAN = "recordBean";
@ConfigParam(name = "skip-line-count", defaultVal = "0")
private int skipLines;
@ConfigParam(name = "fields-in-message", defaultVal = "0")
private boolean fieldsInMessage;
@ConfigParam(defaultVal = "false")
private boolean validateHeader;
@ConfigParam(defaultVal = "false")
private boolean strict;
private String overFlowFromLastRecord = "";
public int getSkipLines() {
if (skipLines < 0) {
return 0;
} else {
return skipLines;
}
}
public boolean fieldsInMessage() {
return fieldsInMessage;
}
public boolean validateHeader() {
return validateHeader;
}
/**
* Get the default record element name.
*
* @return The default record element name.
*/
public String getRecordElementName() {
return recordElementName;
}
public RecordMetaData getRecordMetaData() {
return vfRecordMetaData.getRecordMetaData();
}
/**
* Get the {@link RecordMetaData} instance for the specified fields.
* @param fieldValues The fields.
* @return The RecordMetaData instance.
*/
public RecordMetaData getRecordMetaData(List<String> fieldValues) {
return vfRecordMetaData.getRecordMetaData(fieldValues);
}
/**
* Is the parser configured to parse multiple record types.
* @return True if the parser configured to parse multiple record types, otherwise false.
*/
public boolean isMultiTypeRecordSet() {
return vfRecordMetaData.isMultiTypeRecordSet();
}
/**
* Is this parser instance strict.
*
* @return True if the parser is strict, otherwise false.
*/
public boolean strict() {
return strict;
}
public void addVisitors(VisitorConfigMap visitorMap) {
if (bindBeanId != null && bindBeanClass != null) {
Bean bean;
if (fieldsInMessage) {
throw new SmooksConfigurationException("Unsupported reader based bean binding config. Not supported when fields are defined in message. See 'fieldsInMessage' attribute.");
}
if (vfRecordMetaData.isMultiTypeRecordSet()) {
throw new SmooksConfigurationException(
"Unsupported reader based bean binding config for a multi record type record set. "
+ "Only supported for single record type record sets. Use <jb:bean> configs for multi binding record type record sets.");
}
if (bindingType == BindingType.LIST) {
Bean listBean = new Bean(ArrayList.class, bindBeanId,
SmooksResourceConfiguration.DOCUMENT_FRAGMENT_SELECTOR);
bean = listBean.newBean(bindBeanClass, recordElementName);
listBean.bindTo(bean);
addFieldBindings(bean);
listBean.addVisitors(visitorMap);
} else if (bindingType == BindingType.MAP) {
if (bindMapKeyField == null) {
throw new SmooksConfigurationException(
"'MAP' Binding must specify a 'keyField' property on the binding configuration.");
}
vfRecordMetaData.getRecordMetaData().assertValidFieldName(bindMapKeyField);
Bean mapBean = new Bean(LinkedHashMap.class, bindBeanId,
SmooksResourceConfiguration.DOCUMENT_FRAGMENT_SELECTOR);
Bean recordBean = new Bean(bindBeanClass, RECORD_BEAN, recordElementName);
MapBindingWiringVisitor wiringVisitor = new MapBindingWiringVisitor(bindMapKeyField, bindBeanId);
addFieldBindings(recordBean);
mapBean.addVisitors(visitorMap);
recordBean.addVisitors(visitorMap);
visitorMap.addVisitor(wiringVisitor, recordElementName, null, false);
} else {
bean = new Bean(bindBeanClass, bindBeanId, recordElementName);
addFieldBindings(bean);
bean.addVisitors(visitorMap);
}
}
}
@Initialize
public final void fixupRecordDelimiter() {
if (recordDelimiter == null) {
return;
}
// Fixup the record delimiter...
if (recordDelimiter.startsWith("regex:")) {
recordDelimiterPattern = Pattern.compile(recordDelimiter.substring("regex:".length()),
(Pattern.MULTILINE | Pattern.DOTALL));
} else {
recordDelimiter = removeSpecialCharEncodeString(recordDelimiter, "\\n", '\n');
recordDelimiter = removeSpecialCharEncodeString(recordDelimiter, "\\r", '\r');
recordDelimiter = removeSpecialCharEncodeString(recordDelimiter, "\\t", '\t');
recordDelimiter = XmlUtil.removeEntities(recordDelimiter);
}
}
@Initialize
public final void buildRecordMetaData() {
vfRecordMetaData = new VariableFieldRecordMetaData(recordElementName, fields);
}
/**
* Read a record from the specified reader (up to the next recordDelimiter).
*
* @param recordReader The record {@link Reader}.
* @param recordBuffer The record buffer into which the record is read.
* @throws IOException Error reading record.
*/
public void readRecord(Reader recordReader, StringBuilder recordBuffer, int recordNumber) throws IOException {
recordBuffer.setLength(0);
recordBuffer.append(overFlowFromLastRecord);
RecordBoundaryLocator boundaryLocator;
if (recordDelimiterPattern != null) {
boundaryLocator = new RegexRecordBoundaryLocator(recordBuffer, recordNumber);
} else {
boundaryLocator = new SimpleRecordBoundaryLocator(recordBuffer, recordNumber);
}
int c;
while ((c = recordReader.read()) != -1) {
if (recordBuffer.length() == 0) {
if (c == '\n' || c == '\r') {
// A leading CR or LF... ignore...
continue;
}
}
recordBuffer.append((char) c);
if (boundaryLocator.atEndOfRecord()) {
break;
}
}
overFlowFromLastRecord = boundaryLocator.getOverflowCharacters();
}
private void addFieldBindings(Bean bean) {
for (FieldMetaData fieldMetaData : vfRecordMetaData.getRecordMetaData().getFields()) {
if (!fieldMetaData.ignore()) {
bean.bindTo(fieldMetaData.getName(), recordElementName + "/" + fieldMetaData.getName());
}
}
}
private static String removeSpecialCharEncodeString(String string, String encodedString, char replaceChar) {
return string.replace(encodedString, new String(new char[] { replaceChar }));
}
private class MapBindingWiringVisitor implements DOMVisitAfter, SAXVisitAfter, Consumer {
private MVELExpressionEvaluator keyExtractor = new MVELExpressionEvaluator();
private String mapBindingKey;
private MapBindingWiringVisitor(String bindKeyField, String mapBindingKey) {
keyExtractor.setExpression(RECORD_BEAN + "." + bindKeyField);
this.mapBindingKey = mapBindingKey;
}
public void visitAfter(Element element, ExecutionContext executionContext) throws SmooksException {
wireObject(executionContext);
}
public void visitAfter(SAXElement element, ExecutionContext executionContext) throws SmooksException,
IOException {
wireObject(executionContext);
}
private void wireObject(ExecutionContext executionContext) {
BeanContext beanContext = executionContext.getBeanContext();
Map<String, Object> beanMap = beanContext.getBeanMap();
Object key = keyExtractor.getValue(beanMap);
@SuppressWarnings("unchecked")
// TODO: Optimize to use the BeanId object
Map<Object, Object> map = (Map<Object, Object>) beanContext.getBean(mapBindingKey);
Object record = beanContext.getBean(RECORD_BEAN);
map.put(key, record);
}
public boolean consumes(Object object) {
if (keyExtractor.getExpression().indexOf(object.toString()) != -1) {
return true;
}
return false;
}
}
private class SimpleRecordBoundaryLocator extends RecordBoundaryLocator {
private SimpleRecordBoundaryLocator(StringBuilder recordBuffer, int recordNumber) {
super(recordBuffer, recordNumber);
}
@Override
boolean atEndOfRecord() {
int builderLen = recordBuffer.length();
char lastChar = recordBuffer.charAt(builderLen - 1);
if (recordDelimiter != null) {
int stringLen = recordDelimiter.length();
if (builderLen < stringLen) {
return false;
}
int stringIndx = 0;
for (int i = (builderLen - stringLen); i < builderLen; i++) {
if (recordBuffer.charAt(i) != recordDelimiter.charAt(stringIndx)) {
return false;
}
stringIndx++;
}
if (!keepDelimiter) {
// Strip off the delimiter from the end before returning...
recordBuffer.setLength(builderLen - stringLen);
}
return true;
} else if (lastChar == '\r' || lastChar == '\n') {
if (!keepDelimiter) {
// Strip off the delimiter from the end before returning...
recordBuffer.setLength(builderLen - 1);
}
return true;
}
return false;
}
@Override
String getOverflowCharacters() {
return "";
}
}
private class RegexRecordBoundaryLocator extends RecordBoundaryLocator {
private int startFindIndex;
private int endRecordIndex;
private String overFlow = "";
protected RegexRecordBoundaryLocator(StringBuilder recordBuffer, int recordNumber) {
super(recordBuffer, recordNumber);
startFindIndex = recordBuffer.length();
}
@Override
boolean atEndOfRecord() {
Matcher matcher = recordDelimiterPattern.matcher(recordBuffer);
if (matcher.find(startFindIndex)) {
if (recordNumber == 1 && startFindIndex == 0) {
// Need to find the second instance of the pattern in the
// first record buffer
// The second instance marks the start of the second record,
// which will be captured
// as overflow from this record read and will be auto added
// to the read of the next record.
startFindIndex = matcher.end();
return false;
} else {
// For records following the first record, we already have
// the start so we just need to find
// the first instance of the pattern, which marks the start
// of the next record, which again
// will be captured as overflow from this record read and
// will be auto added to the read of
// the next record.
endRecordIndex = matcher.start();
overFlow = recordBuffer.substring(endRecordIndex);
recordBuffer.setLength(endRecordIndex);
return true;
}
}
return false;
}
@Override
String getOverflowCharacters() {
return overFlow;
}
}
private abstract class RecordBoundaryLocator {
protected StringBuilder recordBuffer;
protected int recordNumber;
protected RecordBoundaryLocator(StringBuilder recordBuffer, int recordNumber) {
this.recordBuffer = recordBuffer;
this.recordNumber = recordNumber;
}
abstract boolean atEndOfRecord();
abstract String getOverflowCharacters();
}
}