/** * Copyright 2011-2017 Asakusa Framework Team. * * 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.asakusafw.runtime.io.text.driver; import java.io.IOException; import java.text.MessageFormat; import java.util.List; import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.asakusafw.runtime.io.text.FieldReader; import com.asakusafw.runtime.io.text.TextFormatException; import com.asakusafw.runtime.io.text.TextInput; import com.asakusafw.runtime.io.text.TextUtil; final class InputDriver<T> implements TextInput<T> { static final Log LOG = LogFactory.getLog(InputDriver.class); private static final String NOT_AVAILABLE = "N/A"; private final FieldReader reader; private final String path; private final Class<?> dataType; private final FieldDriver<T, ?>[] fields; private final HeaderType.Input header; private final boolean trimExtraInput; private final boolean skipExtraEmptyInput; private final ErrorAction onLessInput; private final ErrorAction onMoreInput; private final boolean fromTextHead; private boolean requireConsumeHeader; private final TrimBuffer trimmer = new TrimBuffer(); @SuppressWarnings("unchecked") InputDriver( FieldReader reader, String path, Class<? extends T> dataType, List<FieldDriver<T, ?>> fields, HeaderType.Input header, boolean trimExtraInput, boolean skipExtraEmptyInput, ErrorAction onLessInput, ErrorAction onMoreInput, boolean fromTextHead) { this.reader = reader; this.path = path; this.dataType = dataType; this.fields = (FieldDriver<T, ?>[]) fields.toArray(new FieldDriver<?, ?>[fields.size()]); this.header = header; this.trimExtraInput = trimExtraInput; this.skipExtraEmptyInput = skipExtraEmptyInput; this.onLessInput = onLessInput; this.onMoreInput = onMoreInput; this.fromTextHead = fromTextHead; this.requireConsumeHeader = fromTextHead && header != HeaderType.Input.NEVER; } @Override public long getLineNumber() { return fromTextHead ? reader.getRecordLineNumber() : -1L; } @Override public long getRecordIndex() { return fromTextHead ? reader.getRecordIndex() : -1L; } @Override public boolean readTo(T model) throws IOException { try { if (reader.nextRecord() == false) { return false; } if (LOG.isTraceEnabled()) { LOG.trace(String.format( "reading record: path=%s, line=%,d, fields=%s", path, getLineNumberMessage(), collectFields())); reader.rewindFields(); } if (requireConsumeHeader) { requireConsumeHeader = false; if (doHeaderCheck() == false) { return false; } } process(model); return true; } catch (TextFormatException e) { throw new IOException(MessageFormat.format( "text format is not valid: path={0}, line={1}, row={2}", path != null ? path : NOT_AVAILABLE, getLineNumberMessage(), getRecordIndexMessage()), e); } } private void process(T model) throws IOException { int lessCount = 0; for (FieldDriver<T, ?> field : fields) { boolean success = processField(model, field); if (success == false) { lessCount++; } } if (lessCount == 0) { checkRest(); } else { handleLess(lessCount); } } private <P> boolean processField(T model, FieldDriver<T, P> field) throws IOException { P property = field.extractor.apply(model); FieldAdapter<? super P> adapter = field.adapter; while (reader.nextField()) { CharSequence value = reader.getContent(); if (value != null) { if (field.trimInput) { value = trimmer.wrap(value); } if (value.length() == 0 && field.skipEmptyInput) { if (LOG.isTraceEnabled()) { LOG.trace(String.format( "skip empty field: path=%s, line=%,d, row=%,d, column=%,d", path, getLineNumberMessage(), getRecordIndexMessage(), getFieldIndexMessage())); } continue; } } try { adapter.parse(value, property); } catch (MalformedFieldException e) { adapter.clear(property); handleMalformed(field, value, e); } return true; } adapter.clear(property); return false; } private void checkRest() throws IOException { if (onMoreInput == ErrorAction.IGNORE) { return; } int count = countRest(); if (count == 0) { return; } handle(onMoreInput, null, MessageFormat.format( "record has {0} (of {1}) fields: path={2}, line={3}, row={4}, fields={5}", count + fields.length, fields.length, path != null ? path : NOT_AVAILABLE, getLineNumberMessage(), getRecordIndexMessage(), collectFields())); } private void handleLess(int lessCount) throws IOException { if (onLessInput == ErrorAction.IGNORE) { return; } handle(onLessInput, null, MessageFormat.format( "record has {0} (of {1}) fields: path={2}, line={3}, row={4}, fields={5}", fields.length - lessCount, fields.length, path != null ? path : NOT_AVAILABLE, getLineNumberMessage(), getRecordIndexMessage(), collectFields())); } private void handleMalformed( FieldDriver<?, ?> field, CharSequence value, MalformedFieldException cause) throws IOException { if (field.onMalformedInput == ErrorAction.IGNORE) { return; } handle(field.onMalformedInput, cause, MessageFormat.format( "field \"{0}\" (in {1}) is malformed: path={2}, line={3}, row={4}, column={5}, content={6}", field.name, dataType.getSimpleName(), path != null ? path : NOT_AVAILABLE, getLineNumberMessage(), getRecordIndexMessage(), getFieldIndexMessage(), value == null ? "null" : TextUtil.quote(value))); //$NON-NLS-1$ } private void handle(ErrorAction action, Exception cause, String message) throws IOException { switch (action) { case REPORT: LOG.warn(message, cause); break; case ERROR: throw new IOException(message, cause); default: throw new AssertionError(action); } } private boolean doHeaderCheck() throws IOException { // if the first line is filtered out, we never consume headers if (reader.getRecordLineNumber() != 0L) { return true; } if (testConsumeHeader()) { return reader.nextRecord(); } else { reader.rewindFields(); return true; } } private boolean testConsumeHeader() throws IOException { switch (header) { case ALWAYS: return true; case OPTIONAL: return compareHeader(); default: throw new AssertionError(header); } } private boolean compareHeader() throws IOException { int matched = 0; for (FieldDriver<?, ?> field : fields) { while (reader.nextField()) { CharSequence value = reader.getContent(); String label = field.name; if (value != null) { if (trimExtraInput) { value = trimmer.wrap(value); label = label.trim(); } if (value.length() == 0 && field.skipEmptyInput) { if (LOG.isTraceEnabled()) { LOG.trace(String.format( "skip empty header field: path=%s, column=%,d", path, reader.getFieldIndex())); } continue; } } if (value == null || label.contentEquals(value) == false) { if (LOG.isDebugEnabled()) { LOG.debug(String.format( "header mismatch: path=%s, column=%,d, expected=%s, appeared=%s", //$NON-NLS-1$ path, reader.getFieldIndex(), TextUtil.quote(field.name), value == null ? "null" : TextUtil.quote(value))); //$NON-NLS-1$ } return false; } matched++; break; } } if (checkHeaderFieldCount(matched, onLessInput) == false) { return false; } if (checkHeaderFieldCount(fields.length + countRest(), onMoreInput) == false) { return false; } return true; } private boolean checkHeaderFieldCount(int count, ErrorAction action) { if (count == fields.length || action == ErrorAction.IGNORE) { return true; } String message = MessageFormat.format( "header has {0} (of {1}) fields: path={2}", count, fields.length, path != null ? path : NOT_AVAILABLE); switch (action) { case REPORT: LOG.warn(message); return true; case ERROR: LOG.debug(message); return false; default: throw new AssertionError(action); } } private String collectFields() { try { reader.rewindFields(); StringBuilder buffer = new StringBuilder(); buffer.append('{'); while (reader.nextField()) { if (buffer.length() > 1) { buffer.append(", "); //$NON-NLS-1$ } CharSequence content = reader.getContent(); if (content == null) { buffer.append((Object) null); } else { TextUtil.quoteTo(content, buffer); } } buffer.append('}'); return buffer.toString(); } catch (IOException e) { if (LOG.isDebugEnabled()) { LOG.debug("error occurred while peeking the current line", e); //$NON-NLS-1$ } return NOT_AVAILABLE; } } private int countRest() throws IOException { int count = 0; while (reader.nextField()) { if (skipExtraEmptyInput) { CharSequence cs = reader.getContent(); if (cs != null) { if (trimExtraInput) { cs = trimmer.wrap(cs); } if (cs.length() == 0) { continue; } } } count++; } return count; } private Object getLineNumberMessage() { return getIndexMessage(getLineNumber()); } private Object getRecordIndexMessage() { return getIndexMessage(getRecordIndex()); } private Object getFieldIndexMessage() { return getIndexMessage(reader.getFieldIndex()); } private Object getIndexMessage(long index) { return index < 0 ? NOT_AVAILABLE : index + 1; } @Override public void close() throws IOException { reader.close(); } @Override public String toString() { return String.format("InputDriver(path=%s, reader=%s)", path, reader); //$NON-NLS-1$ } static class FieldDriver<TRecord, TProperty> { final String name; final Function<? super TRecord, ? extends TProperty> extractor; final FieldAdapter<? super TProperty> adapter; final boolean trimInput; final boolean skipEmptyInput; final ErrorAction onMalformedInput; FieldDriver( String name, Function<? super TRecord, ? extends TProperty> extractor, FieldAdapter<? super TProperty> adapter, boolean trimInput, boolean skipEmptyInput, ErrorAction onMalformedInput) { this.name = name; this.extractor = extractor; this.adapter = adapter; this.trimInput = trimInput; this.skipEmptyInput = skipEmptyInput; this.onMalformedInput = onMalformedInput; } } private static final class TrimBuffer implements CharSequence { private CharSequence parent; private int offset; private int length; TrimBuffer() { this.parent = ""; //$NON-NLS-1$ this.offset = 0; this.length = 0; } CharSequence wrap(CharSequence cs) { int newLength = cs.length(); int newOffset = TextUtil.countLeadingWhitespaces(cs, 0, newLength); newLength -= newOffset; newLength -= TextUtil.countTrailingWhitespaces(cs, newOffset, newLength); if (newLength == 0) { return ""; //$NON-NLS-1$ } else if (newOffset == 0 && newLength == cs.length()) { return cs; } else { parent = cs; offset = newOffset; length = newLength; return this; } } @Override public int length() { return length; } @Override public char charAt(int index) { if (index < 0 || index >= length) { throw new IndexOutOfBoundsException(); } return parent.charAt(index + offset); } @Override public CharSequence subSequence(int start, int end) { if (start < 0 || start > end || end > length) { throw new IndexOutOfBoundsException(); } return parent.subSequence(start + offset, end + offset); } @Override public String toString() { return parent.subSequence(offset, offset + length).toString(); } } }