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}