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