001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.io.input;
018
019
020import java.io.IOException;
021import java.io.InputStream;
022
023/**
024 * A filtering input stream that ensures the content will have unix-style line endings, LF.
025 *
026 * @since 2.5
027 */
028public class UnixLineEndingInputStream extends InputStream {
029
030    private boolean slashNSeen = false;
031
032    private boolean slashRSeen = false;
033
034    private boolean eofSeen = false;
035
036    private final InputStream target;
037
038    private final boolean ensureLineFeedAtEndOfFile;
039
040    /**
041     * Create an input stream that filters another stream
042     *
043     * @param in                        The input stream to wrap
044     * @param ensureLineFeedAtEndOfFile true to ensure that the file ends with LF
045     */
046    public UnixLineEndingInputStream( InputStream in, boolean ensureLineFeedAtEndOfFile ) {
047        this.target = in;
048        this.ensureLineFeedAtEndOfFile = ensureLineFeedAtEndOfFile;
049    }
050
051    /**
052     * Reads the next item from the target, updating internal flags in the process
053     * @return the next int read from the target stream
054     * @throws IOException upon error
055     */
056    private int readWithUpdate() throws IOException {
057        final int target = this.target.read();
058        eofSeen = target == -1;
059        if ( eofSeen ) {
060            return target;
061        }
062        slashNSeen = target == '\n';
063        slashRSeen = target == '\r';
064        return target;
065    }
066
067    /**
068     * {@inheritDoc}
069     */
070    @Override
071    public int read() throws IOException {
072        boolean previousWasSlashR = slashRSeen;
073        if ( eofSeen ) {
074            return eofGame(previousWasSlashR);
075        }
076        else {
077            int target = readWithUpdate();
078            if ( eofSeen ) {
079                return eofGame(previousWasSlashR);
080            }
081            if (slashRSeen)
082            {
083                return '\n';
084            }
085
086            if ( previousWasSlashR && slashNSeen){
087                return read();
088            }
089
090            return target;
091        }
092    }
093
094    /**
095     * Handles the eof-handling at the end of the stream
096     * @param previousWasSlashR Indicates if the last seen was a \r
097     * @return The next char to output to the stream
098     */
099    private int eofGame(boolean previousWasSlashR) {
100        if ( previousWasSlashR || !ensureLineFeedAtEndOfFile ) {
101            return -1;
102        }
103        if ( !slashNSeen ) {
104            slashNSeen = true;
105            return '\n';
106        } else {
107            return -1;
108        }
109    }
110
111    /**
112     * Closes the stream. Also closes the underlying stream.
113     * @throws IOException upon error
114     */
115    @Override
116    public void close() throws IOException {
117        super.close();
118        target.close();
119    }
120
121    /**
122     * {@inheritDoc}
123     */
124    @Override
125    public synchronized void mark( int readlimit ) {
126        throw new UnsupportedOperationException( "Mark notsupported" );
127    }
128}