001/*
002 * JRecordBind, fixed-length file (un)marshaller
003 * Copyright 2019, Federico Fissore, and individual contributors. See
004 * AUTHORS.txt in the distribution for a full listing of individual
005 * contributors.
006 *
007 * This is free software; you can redistribute it and/or modify it
008 * under the terms of the GNU Lesser General Public License as
009 * published by the Free Software Foundation; either version 2.1 of
010 * the License, or (at your option) any later version.
011 *
012 * This software is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this software; if not, write to the Free
019 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021 */
022
023package org.fissore.jrecordbind;
024
025import org.fissore.jrecordbind.RecordDefinition.Property;
026
027import java.io.BufferedReader;
028import java.io.Reader;
029import java.util.*;
030import java.util.logging.Level;
031import java.util.logging.Logger;
032import java.util.regex.Matcher;
033import java.util.regex.Pattern;
034import java.util.stream.Stream;
035import java.util.stream.StreamSupport;
036
037import static org.fissore.jrecordbind.Utils.newInstanceOf;
038
039/**
040 * Transforms an input reader into beans. You can consume the transformed beans
041 * by calling {@link #unmarshallToIterator(Reader)} which returns an Iterator, or
042 * by calling {@link #unmarshallToStream(Reader)}, which returns a Stream.
043 */
044public class Unmarshaller<E> extends AbstractUnMarshaller {
045
046  private class UnmarshallerIterator<T> implements Iterator<T> {
047
048    private final Logger log = Logger.getLogger(UnmarshallerIterator.class.getName());
049
050    private final StringBuilder buffer;
051    private final Pattern globalPattern;
052    private final LineReader lineReader;
053    private final Reader reader;
054    private final RegexGenerator regexGenerator;
055    private final PropertyUtils propertyUtils;
056    private Object currentRecord;
057
058    public UnmarshallerIterator(StringBuilder buffer, LineReader lineReader, Reader reader) {
059      this.buffer = buffer;
060      this.lineReader = lineReader;
061      this.reader = reader;
062      this.regexGenerator = new RegexGenerator();
063      this.globalPattern = regexGenerator.deepPattern(definition);
064      this.propertyUtils = new PropertyUtils();
065    }
066
067    private void readNext() {
068      if (currentRecord != null) {
069        return;
070      }
071
072      String current;
073      Matcher matcher;
074      boolean found;
075      // Try to match a record on the current buffer.
076      // As long as there is no match, or the match ends at the buffer's end,
077      // and there is one more line, read a new line, and add it to the buffer.
078      // Then try again
079      while ((!(found = (matcher = globalPattern.matcher(buffer)).find()) || matcher.end() == (buffer.length() - 1))
080        && (current = lineReader
081        .readLine(reader, padders.get(definition.getDefaultPadder()), definition.getPropertyDelimiter(), definition.getLength(), definition.getLineSeparator())) != null) {
082        buffer.append(current).append("\n");
083      }
084
085      if (found) {
086        try {
087          Object record = newInstanceOf(definition.getClassName());
088          StringBuilder currentBuffer = new StringBuilder(buffer.substring(matcher.start(), matcher.end()));
089
090          // Parse the data from currentBuffer to record according to definition
091          recursive(record, definition, currentBuffer);
092
093          buffer.delete(matcher.start(), matcher.end() + 1);
094
095          currentRecord = record;
096        } catch (Exception e) {
097          throw new RuntimeException(e);
098        }
099      }
100
101      log.log(Level.FINE, "A new record has been read");
102    }
103
104    public boolean hasNext() {
105      readNext();
106      return currentRecord != null;
107    }
108
109    @SuppressWarnings("unchecked")
110    public T next() {
111      readNext();
112      if (currentRecord == null) {
113        throw new NoSuchElementException();
114      }
115      T current = (T) currentRecord;
116      currentRecord = null;
117      return current;
118    }
119
120    @SuppressWarnings("unchecked")
121    private void recursive(Object record, RecordDefinition currentDefinition, StringBuilder currentBuffer) {
122
123      // If the current definition has direct properties, match and read them
124      Matcher matcher = regexGenerator.localPattern(currentDefinition).matcher(currentBuffer);
125      if (!matcher.find()) {
126        return;
127      }
128
129      int groupCount = 1;
130
131      for (Property property : currentDefinition.getProperties()) {
132        String stringValue = matcher.group(groupCount);
133        String unpaddedStringValue = getPadder(currentDefinition, property).unpad(stringValue);
134        Object convertedValue = getConverter(property).convert(unpaddedStringValue);
135        propertyUtils.setProperty(record, property.getName(), convertedValue);
136        groupCount++;
137      }
138      // Then delete the matched section
139      currentBuffer.delete(matcher.start(), matcher.end());
140
141      // If the current definition has subrecords, handle them
142      Matcher subMatcher;
143      for (RecordDefinition subDefinition : currentDefinition.getSubRecords()) {
144        int matchedRows = 0;
145        while ((subMatcher = regexGenerator.deepPattern(subDefinition).matcher(currentBuffer)).find()
146          && (subDefinition.getMaxOccurs() == -1 || matchedRows < subDefinition.getMaxOccurs())) {
147
148          // Recurursively call this function for the found subrecord
149          StringBuilder subBuffer = new StringBuilder(currentBuffer.substring(subMatcher.start(), subMatcher.end()));
150          Object subRecord = newInstanceOf(subDefinition.getClassName());
151          recursive(subRecord, subDefinition, subBuffer);
152          currentBuffer.delete(subMatcher.start(), subMatcher.end());
153
154          // Set, or add to the collection the property found above
155          Object property = propertyUtils.getProperty(record, subDefinition.getSetterName());
156          if (property instanceof Collection) {
157            Collection<Object> collection = (Collection<Object>) property;
158            collection.add(subRecord);
159          } else {
160            propertyUtils.setProperty(record, subDefinition.getSetterName(), subRecord);
161          }
162          matchedRows++;
163        }
164      }
165    }
166
167    public void remove() {
168    }
169
170  }
171
172  private final StringBuilder buffer;
173  private final LineReader lineReader;
174
175  /**
176   * Creates a new Unmarshaller, with the specified record definition and
177   * using the default {@link LineReader} implementation<br>
178   *
179   * @param definition the record definition
180   */
181  public Unmarshaller(RecordDefinition definition) {
182    this(definition, new SimpleLineReader());
183  }
184
185  /**
186   * Creates a new Unmarshaller, with the specified record definition and
187   * using the specified {@link LineReader} implementation
188   *
189   * @param definition the record definition
190   * @param lineReader a custom implementation of the LineReader
191   */
192  public Unmarshaller(RecordDefinition definition, LineReader lineReader) {
193    this(definition, lineReader, new HashMap<>(), new HashMap<>());
194  }
195
196  /**
197   * Creates a new Unmarshaller, with the specified record definition and
198   * using the specified {@link LineReader} implementation, and with user provided
199   * instances of converters and padders.
200   *
201   * @param definition the record definition
202   * @param lineReader a custom implementation of the LineReader
203   * @param converters user provided instances of converters
204   * @param padders    user provided instances of padders
205   */
206  public Unmarshaller(RecordDefinition definition, LineReader lineReader, Map<String, Converter> converters, Map<String, Padder> padders) {
207    super(definition, converters, padders);
208    this.lineReader = lineReader;
209    this.buffer = new StringBuilder();
210  }
211
212  /**
213   * Returns the current internal buffer content. If called right after a
214   * {@link Iterator#next()} call, it will return what JRecordBind wasn't able
215   * to unmarshall. Usually called after the {@link Iterator#hasNext()} has
216   * returned <code>false</code> to report the user about the "junk" found in
217   * the text file
218   *
219   * @return the current "junk" stored in the internal buffer
220   */
221  public String getCurrentJunk() {
222    return buffer.toString();
223  }
224
225  /**
226   * Unmarshalls the input fixed-length file, a bean at a time
227   *
228   * @param input the input fixed-length file
229   * @return an Iterator: each next() call will give back the next bean
230   */
231  public Iterator<E> unmarshallToIterator(Reader input) {
232    return new UnmarshallerIterator<>(buffer, lineReader, new BufferedReader(input));
233  }
234
235  /**
236   * Like {@link Unmarshaller#unmarshallToIterator(Reader)}, but returns a Stream instead of an Iterator
237   *
238   * @param input the input fixed-length file
239   * @return a stream of beans
240   */
241  public Stream<E> unmarshallToStream(Reader input) {
242    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(unmarshallToIterator(input), Spliterator.ORDERED), false);
243  }
244
245}