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( final InputStream in, final 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        final boolean previousWasSlashR = slashRSeen;
073        if ( eofSeen ) {
074            return eofGame(previousWasSlashR);
075        }
076        final int target = readWithUpdate();
077        if ( eofSeen ) {
078            return eofGame(previousWasSlashR);
079        }
080        if (slashRSeen)
081        {
082            return '\n';
083        }
084
085        if ( previousWasSlashR && slashNSeen){
086            return read();
087        }
088
089        return target;
090    }
091
092    /**
093     * Handles the eof-handling at the end of the stream
094     * @param previousWasSlashR Indicates if the last seen was a \r
095     * @return The next char to output to the stream
096     */
097    private int eofGame(final boolean previousWasSlashR) {
098        if ( previousWasSlashR || !ensureLineFeedAtEndOfFile ) {
099            return -1;
100        }
101        if ( !slashNSeen ) {
102            slashNSeen = true;
103            return '\n';
104        }
105        return -1;
106    }
107
108    /**
109     * Closes the stream. Also closes the underlying stream.
110     * @throws IOException upon error
111     */
112    @Override
113    public void close() throws IOException {
114        super.close();
115        target.close();
116    }
117
118    /**
119     * {@inheritDoc}
120     */
121    @Override
122    public synchronized void mark( final int readlimit ) {
123        throw new UnsupportedOperationException( "Mark notsupported" );
124    }
125}