001/*
002 * Copyright 2006-2007 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.springframework.batch.item.file;
017
018import java.io.BufferedReader;
019import java.io.IOException;
020import java.io.InputStreamReader;
021import java.io.Reader;
022import java.io.UnsupportedEncodingException;
023
024import org.springframework.core.io.Resource;
025
026/**
027 * A {@link BufferedReaderFactory} useful for reading simple binary (or text)
028 * files with no line endings, such as those produced by mainframe copy books.
029 * The reader splits a stream up across fixed line endings (rather than the
030 * usual convention based on plain text). The line endings are discarded, just
031 * as with the default plain text implementation.
032 * 
033 * @author Dave Syer
034 * 
035 * @since 2.1
036 */
037public class SimpleBinaryBufferedReaderFactory implements BufferedReaderFactory {
038
039        /**
040         * The default line ending value.
041         */
042        private static final String DEFAULT_LINE_ENDING = "\n";
043
044        private String lineEnding = DEFAULT_LINE_ENDING;
045
046        /**
047         * @param lineEnding {@link String} indicating what defines the end of a "line".
048         */
049        public void setLineEnding(String lineEnding) {
050                this.lineEnding = lineEnding;
051        }
052
053    @Override
054        public BufferedReader create(Resource resource, String encoding) throws UnsupportedEncodingException, IOException {
055                return new BinaryBufferedReader(new InputStreamReader(resource.getInputStream(), encoding), lineEnding);
056        }
057
058        /**
059         * BufferedReader extension that splits lines based on a line ending, rather
060         * than the usual plain text conventions.
061         * 
062         * @author Dave Syer
063         * 
064         */
065        private final class BinaryBufferedReader extends BufferedReader {
066
067                private final String ending;
068
069                /**
070                 * @param in
071                 */
072                private BinaryBufferedReader(Reader in, String ending) {
073                        super(in);
074                        this.ending = ending;
075                }
076
077                @Override
078                public String readLine() throws IOException {
079
080                        StringBuilder buffer = null;
081
082                        synchronized (lock) {
083
084                                int next = read();
085                                if (next == -1) {
086                                        return null;
087                                }
088
089                                buffer = new StringBuilder();
090                                StringBuilder candidateEnding = new StringBuilder();
091
092                                while (!isEndOfLine(buffer, candidateEnding, next)) {
093                                        next = read();
094                                }
095                                buffer.append(candidateEnding);
096
097                        }
098
099                        if (buffer != null && buffer.length() > 0) {
100                                return buffer.toString();
101                        }
102                        return null;
103
104                }
105
106                /**
107                 * Check for end of line and accumulate a buffer for next time.
108                 * 
109                 * @param buffer the current line excluding the candidate ending
110                 * @param candidate a buffer containing accumulated state
111                 * @param next the next character (or -1 for end of file)
112                 * @return true if the values together signify the end of a file
113                 */
114                private boolean isEndOfLine(StringBuilder buffer, StringBuilder candidate, int next) {
115
116                        if (next == -1) {
117                                return true;
118                        }
119
120                        char c = (char) next;
121                        if (ending.charAt(0) == c || candidate.length() > 0) {
122                                candidate.append(c);
123                        }
124
125                        if (candidate.length() == 0) {
126                                buffer.append(c);
127                                return false;
128                        }
129
130                        boolean end = ending.equals(candidate.toString());
131                        if (end) {
132                                candidate.delete(0, candidate.length());
133                        }
134                        else if (candidate.length() >= ending.length()) {
135                                buffer.append(candidate);
136                                candidate.delete(0, candidate.length());
137                        }
138
139                        return end;
140
141                }
142        }
143
144}