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