001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.spi.connector.transport;
020
021import java.io.ByteArrayOutputStream;
022import java.io.File;
023import java.io.IOException;
024import java.io.OutputStream;
025import java.io.UncheckedIOException;
026import java.net.URI;
027import java.nio.charset.StandardCharsets;
028import java.nio.file.Files;
029import java.nio.file.Path;
030import java.nio.file.StandardOpenOption;
031import java.util.Collections;
032import java.util.HashMap;
033import java.util.Map;
034
035/**
036 * A task to download a resource from the remote repository.
037 *
038 * @see Transporter#get(GetTask)
039 */
040public final class GetTask extends TransportTask {
041
042    private Path dataPath;
043
044    private boolean resume;
045
046    private ByteArrayOutputStream dataBytes;
047
048    private Map<String, String> checksums;
049
050    /**
051     * Creates a new task for the specified remote resource.
052     *
053     * @param location The relative location of the resource in the remote repository, must not be {@code null}.
054     */
055    public GetTask(URI location) {
056        checksums = Collections.emptyMap();
057        setLocation(location);
058    }
059
060    /**
061     * Opens an output stream to store the downloaded data. Depending on {@link #getDataFile()}, this stream writes
062     * either to a file on disk or a growable buffer in memory. It's the responsibility of the caller to close the
063     * provided stream.
064     *
065     * @return The output stream for the data, never {@code null}. The stream is unbuffered.
066     * @throws IOException If the stream could not be opened.
067     */
068    public OutputStream newOutputStream() throws IOException {
069        return newOutputStream(false);
070    }
071
072    /**
073     * Opens an output stream to store the downloaded data. Depending on {@link #getDataFile()}, this stream writes
074     * either to a file on disk or a growable buffer in memory. It's the responsibility of the caller to close the
075     * provided stream.
076     *
077     * @param resume {@code true} if the download resumes from the byte offset given by {@link #getResumeOffset()},
078     *            {@code false} if the download starts at the first byte of the resource.
079     * @return The output stream for the data, never {@code null}. The stream is unbuffered.
080     * @throws IOException If the stream could not be opened.
081     */
082    public OutputStream newOutputStream(boolean resume) throws IOException {
083        if (dataPath != null) {
084            if (this.resume && resume) {
085                return Files.newOutputStream(
086                        dataPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
087            } else {
088                return Files.newOutputStream(
089                        dataPath,
090                        StandardOpenOption.CREATE,
091                        StandardOpenOption.WRITE,
092                        StandardOpenOption.TRUNCATE_EXISTING);
093            }
094        }
095        if (dataBytes == null) {
096            dataBytes = new ByteArrayOutputStream(1024);
097        } else if (!resume) {
098            dataBytes.reset();
099        }
100        return dataBytes;
101    }
102
103    /**
104     * Gets the file (if any) where the downloaded data should be stored. If the specified file already exists, it will
105     * be overwritten.
106     *
107     * @return The data file or {@code null} if the data will be buffered in memory.
108     * @deprecated Use {@link #getDataPath()} instead.
109     */
110    @Deprecated
111    public File getDataFile() {
112        return dataPath != null ? dataPath.toFile() : null;
113    }
114
115    /**
116     * Gets the file (if any) where the downloaded data should be stored. If the specified file already exists, it will
117     * be overwritten.
118     *
119     * @return The data file or {@code null} if the data will be buffered in memory.
120     * @since 2.0.0
121     */
122    public Path getDataPath() {
123        return dataPath;
124    }
125
126    /**
127     * Sets the file where the downloaded data should be stored. If the specified file already exists, it will be
128     * overwritten. Unless the caller can reasonably expect the resource to be small, use of a data file is strongly
129     * recommended to avoid exhausting heap memory during the download.
130     *
131     * @param dataFile The file to store the downloaded data, may be {@code null} to store the data in memory.
132     * @return This task for chaining, never {@code null}.
133     * @deprecated Use {@link #setDataPath(Path)} instead.
134     */
135    @Deprecated
136    public GetTask setDataFile(File dataFile) {
137        return setDataFile(dataFile, false);
138    }
139
140    /**
141     * Sets the file where the downloaded data should be stored. If the specified file already exists, it will be
142     * overwritten. Unless the caller can reasonably expect the resource to be small, use of a data file is strongly
143     * recommended to avoid exhausting heap memory during the download.
144     *
145     * @param dataPath The file to store the downloaded data, may be {@code null} to store the data in memory.
146     * @return This task for chaining, never {@code null}.
147     * @since 2.0.0
148     */
149    public GetTask setDataPath(Path dataPath) {
150        return setDataPath(dataPath, false);
151    }
152
153    /**
154     * Sets the file where the downloaded data should be stored. If the specified file already exists, it will be
155     * overwritten or appended to, depending on the {@code resume} argument and the capabilities of the transporter.
156     * Unless the caller can reasonably expect the resource to be small, use of a data file is strongly recommended to
157     * avoid exhausting heap memory during the download.
158     *
159     * @param dataFile The file to store the downloaded data, may be {@code null} to store the data in memory.
160     * @param resume {@code true} to request resuming a previous download attempt, starting from the current length of
161     *            the data file, {@code false} to download the resource from its beginning.
162     * @return This task for chaining, never {@code null}.
163     * @deprecated Use {@link #setDataPath(Path, boolean)} instead.
164     */
165    @Deprecated
166    public GetTask setDataFile(File dataFile, boolean resume) {
167        return setDataPath(dataFile != null ? dataFile.toPath() : null, resume);
168    }
169
170    /**
171     * Sets the file where the downloaded data should be stored. If the specified file already exists, it will be
172     * overwritten or appended to, depending on the {@code resume} argument and the capabilities of the transporter.
173     * Unless the caller can reasonably expect the resource to be small, use of a data file is strongly recommended to
174     * avoid exhausting heap memory during the download.
175     *
176     * @param dataPath The file to store the downloaded data, may be {@code null} to store the data in memory.
177     * @param resume {@code true} to request resuming a previous download attempt, starting from the current length of
178     *            the data file, {@code false} to download the resource from its beginning.
179     * @return This task for chaining, never {@code null}.
180     * @since 2.0.0
181     */
182    public GetTask setDataPath(Path dataPath, boolean resume) {
183        this.dataPath = dataPath;
184        this.resume = resume;
185        return this;
186    }
187
188    /**
189     * Gets the byte offset within the resource from which the download should resume if supported.
190     *
191     * @return The zero-based index of the first byte to download or {@code 0} for a full download from the start of the
192     *         resource, never negative.
193     */
194    public long getResumeOffset() {
195        if (resume) {
196            if (dataPath != null) {
197                try {
198                    return Files.size(dataPath);
199                } catch (IOException e) {
200                    throw new UncheckedIOException(e);
201                }
202            }
203            if (dataBytes != null) {
204                return dataBytes.size();
205            }
206        }
207        return 0;
208    }
209
210    /**
211     * Gets the data that was downloaded into memory. <strong>Note:</strong> This method may only be called if
212     * {@link #getDataFile()} is {@code null} as otherwise the downloaded data has been written directly to disk.
213     *
214     * @return The possibly empty data bytes, never {@code null}.
215     */
216    public byte[] getDataBytes() {
217        if (dataPath != null || dataBytes == null) {
218            return EMPTY;
219        }
220        return dataBytes.toByteArray();
221    }
222
223    /**
224     * Gets the data that was downloaded into memory as a string. The downloaded data is assumed to be encoded using
225     * UTF-8. <strong>Note:</strong> This method may only be called if {@link #getDataFile()} is {@code null} as
226     * otherwise the downloaded data has been written directly to disk.
227     *
228     * @return The possibly empty data string, never {@code null}.
229     */
230    public String getDataString() {
231        if (dataPath != null || dataBytes == null) {
232            return "";
233        }
234        return new String(dataBytes.toByteArray(), StandardCharsets.UTF_8);
235    }
236
237    /**
238     * Sets the listener that is to be notified during the transfer.
239     *
240     * @param listener The listener to notify of progress, may be {@code null}.
241     * @return This task for chaining, never {@code null}.
242     */
243    public GetTask setListener(TransportListener listener) {
244        super.setListener(listener);
245        return this;
246    }
247
248    /**
249     * Gets the checksums which the remote repository advertises for the resource. The map is keyed by algorithm name
250     * and the values are hexadecimal representations of the corresponding value. <em>Note:</em> This is optional
251     * data that a transporter may return if the underlying transport protocol provides metadata (e.g. HTTP headers)
252     * along with the actual resource data. Checksums returned by this method have kind of
253     * {@link org.eclipse.aether.spi.connector.checksum.ChecksumPolicy.ChecksumKind#REMOTE_INCLUDED}.
254     *
255     * @return The (read-only) checksums advertised for the downloaded resource, possibly empty but never {@code null}.
256     */
257    public Map<String, String> getChecksums() {
258        return checksums;
259    }
260
261    /**
262     * Sets a checksum which the remote repository advertises for the resource. <em>Note:</em> Transporters should only
263     * use this method to record checksum information which is readily available while performing the actual download,
264     * they should not perform additional transfers to gather this data.
265     *
266     * @param algorithm The name of the checksum algorithm (e.g. {@code "SHA-1"}, may be {@code null}.
267     * @param value The hexadecimal representation of the checksum, may be {@code null}.
268     * @return This task for chaining, never {@code null}.
269     */
270    public GetTask setChecksum(String algorithm, String value) {
271        if (algorithm != null) {
272            if (checksums.isEmpty()) {
273                checksums = new HashMap<>();
274            }
275            if (value != null && !value.isEmpty()) {
276                checksums.put(algorithm, value);
277            } else {
278                checksums.remove(algorithm);
279            }
280        }
281        return this;
282    }
283
284    @Override
285    public String toString() {
286        return "<< " + getLocation();
287    }
288}