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}