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 *
017 */
018
019package org.apache.commons.compress.utils;
020
021import java.io.File;
022import java.io.IOException;
023import java.nio.ByteBuffer;
024import java.nio.channels.ClosedChannelException;
025import java.nio.channels.NonWritableChannelException;
026import java.nio.channels.SeekableByteChannel;
027import java.nio.file.Files;
028import java.nio.file.Path;
029import java.nio.file.StandardOpenOption;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Collections;
033import java.util.List;
034import java.util.Objects;
035
036/**
037 * Read-Only Implementation of {@link SeekableByteChannel} that
038 * concatenates a collection of other {@link SeekableByteChannel}s.
039 *
040 * <p>This is a lose port of <a
041 * href="https://github.com/frugalmechanic/fm-common/blob/master/jvm/src/main/scala/fm/common/MultiReadOnlySeekableByteChannel.scala">MultiReadOnlySeekableByteChannel</a>
042 * by Tim Underwood.</p>
043 *
044 * @since 1.19
045 */
046public class MultiReadOnlySeekableByteChannel implements SeekableByteChannel {
047
048    private static final Path[] EMPTY_PATH_ARRAY = {};
049    private final List<SeekableByteChannel> channels;
050    private long globalPosition;
051    private int currentChannelIdx;
052
053    /**
054     * Concatenates the given channels.
055     *
056     * @param channels the channels to concatenate
057     * @throws NullPointerException if channels is null
058     */
059    public MultiReadOnlySeekableByteChannel(final List<SeekableByteChannel> channels) {
060        this.channels = Collections.unmodifiableList(new ArrayList<>(
061            Objects.requireNonNull(channels, "channels must not be null")));
062    }
063
064    @Override
065    public synchronized int read(final ByteBuffer dst) throws IOException {
066        if (!isOpen()) {
067            throw new ClosedChannelException();
068        }
069        if (!dst.hasRemaining()) {
070            return 0;
071        }
072
073        int totalBytesRead = 0;
074        while (dst.hasRemaining() && currentChannelIdx < channels.size()) {
075            final SeekableByteChannel currentChannel = channels.get(currentChannelIdx);
076            final int newBytesRead = currentChannel.read(dst);
077            if (newBytesRead == -1) {
078                // EOF for this channel -- advance to next channel idx
079                currentChannelIdx += 1;
080                continue;
081            }
082            if (currentChannel.position() >= currentChannel.size()) {
083                // we are at the end of the current channel
084                currentChannelIdx++;
085            }
086            totalBytesRead += newBytesRead;
087        }
088        if (totalBytesRead > 0) {
089            globalPosition += totalBytesRead;
090            return totalBytesRead;
091        }
092        return -1;
093    }
094
095    @Override
096    public void close() throws IOException {
097        IOException first = null;
098        for (final SeekableByteChannel ch : channels) {
099            try {
100                ch.close();
101            } catch (final IOException ex) {
102                if (first == null) {
103                    first = ex;
104                }
105            }
106        }
107        if (first != null) {
108            throw new IOException("failed to close wrapped channel", first);
109        }
110    }
111
112    @Override
113    public boolean isOpen() {
114        return channels.stream().allMatch(SeekableByteChannel::isOpen);
115    }
116
117    /**
118     * Returns this channel's position.
119     *
120     * <p>This method violates the contract of {@link SeekableByteChannel#position()} as it will not throw any exception
121     * when invoked on a closed channel. Instead it will return the position the channel had when close has been
122     * called.</p>
123     */
124    @Override
125    public long position() {
126        return globalPosition;
127    }
128
129    /**
130     * set the position based on the given channel number and relative offset
131     *
132     * @param channelNumber  the channel number
133     * @param relativeOffset the relative offset in the corresponding channel
134     * @return global position of all channels as if they are a single channel
135     * @throws IOException if positioning fails
136     */
137    public synchronized SeekableByteChannel position(final long channelNumber, final long relativeOffset) throws IOException {
138        if (!isOpen()) {
139            throw new ClosedChannelException();
140        }
141        long globalPosition = relativeOffset;
142        for (int i = 0; i < channelNumber; i++) {
143            globalPosition += channels.get(i).size();
144        }
145
146        return position(globalPosition);
147    }
148
149    @Override
150    public long size() throws IOException {
151        if (!isOpen()) {
152            throw new ClosedChannelException();
153        }
154        long acc = 0;
155        for (final SeekableByteChannel ch : channels) {
156            acc += ch.size();
157        }
158        return acc;
159    }
160
161    /**
162     * @throws NonWritableChannelException since this implementation is read-only.
163     */
164    @Override
165    public SeekableByteChannel truncate(final long size) {
166        throw new NonWritableChannelException();
167    }
168
169    /**
170     * @throws NonWritableChannelException since this implementation is read-only.
171     */
172    @Override
173    public int write(final ByteBuffer src) {
174        throw new NonWritableChannelException();
175    }
176
177    @Override
178    public synchronized SeekableByteChannel position(final long newPosition) throws IOException {
179        if (newPosition < 0) {
180            throw new IOException("Negative position: " + newPosition);
181        }
182        if (!isOpen()) {
183            throw new ClosedChannelException();
184        }
185
186        globalPosition = newPosition;
187
188        long pos = newPosition;
189
190        for (int i = 0; i < channels.size(); i++) {
191            final SeekableByteChannel currentChannel = channels.get(i);
192            final long size = currentChannel.size();
193
194            final long newChannelPos;
195            if (pos == -1L) {
196                // Position is already set for the correct channel,
197                // the rest of the channels get reset to 0
198                newChannelPos = 0;
199            } else if (pos <= size) {
200                // This channel is where we want to be
201                currentChannelIdx = i;
202                final long tmp = pos;
203                pos = -1L; // Mark pos as already being set
204                newChannelPos = tmp;
205            } else {
206                // newPosition is past this channel.  Set channel
207                // position to the end and substract channel size from
208                // pos
209                pos -= size;
210                newChannelPos = size;
211            }
212
213            currentChannel.position(newChannelPos);
214        }
215        return this;
216    }
217
218    /**
219     * Concatenates the given channels.
220     *
221     * @param channels the channels to concatenate
222     * @throws NullPointerException if channels is null
223     * @return SeekableByteChannel that concatenates all provided channels
224     */
225    public static SeekableByteChannel forSeekableByteChannels(final SeekableByteChannel... channels) {
226        if (Objects.requireNonNull(channels, "channels must not be null").length == 1) {
227            return channels[0];
228        }
229        return new MultiReadOnlySeekableByteChannel(Arrays.asList(channels));
230    }
231
232    /**
233     * Concatenates the given files.
234     *
235     * @param files the files to concatenate
236     * @throws NullPointerException if files is null
237     * @throws IOException if opening a channel for one of the files fails
238     * @return SeekableByteChannel that concatenates all provided files
239     */
240    public static SeekableByteChannel forFiles(final File... files) throws IOException {
241        final List<Path> paths = new ArrayList<>();
242        for (final File f : Objects.requireNonNull(files, "files must not be null")) {
243            paths.add(f.toPath());
244        }
245
246        return forPaths(paths.toArray(EMPTY_PATH_ARRAY));
247    }
248
249    /**
250     * Concatenates the given file paths.
251     * @param paths the file paths to concatenate, note that the LAST FILE of files should be the LAST SEGMENT(.zip)
252     * and these files should be added in correct order (e.g.: .z01, .z02... .z99, .zip)
253     * @return SeekableByteChannel that concatenates all provided files
254     * @throws NullPointerException if files is null
255     * @throws IOException if opening a channel for one of the files fails
256     * @throws IOException if the first channel doesn't seem to hold
257     * the beginning of a split archive
258     * @since 1.22
259     */
260    public static SeekableByteChannel forPaths(final Path... paths) throws IOException {
261        final List<SeekableByteChannel> channels = new ArrayList<>();
262        for (final Path path : Objects.requireNonNull(paths, "paths must not be null")) {
263            channels.add(Files.newByteChannel(path, StandardOpenOption.READ));
264        }
265        if (channels.size() == 1) {
266            return channels.get(0);
267        }
268        return new MultiReadOnlySeekableByteChannel(channels);
269    }
270
271}