blob: 8e3b7fe7a47060742d42e68b1eddaeaf6c273d8f [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.mercury.spi.http.client.retrieve;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.maven.mercury.crypto.api.StreamObserver;
import org.apache.maven.mercury.crypto.api.StreamVerifier;
import org.apache.maven.mercury.crypto.api.StreamVerifierException;
import org.apache.maven.mercury.logging.IMercuryLogger;
import org.apache.maven.mercury.logging.MercuryLoggerManager;
import org.apache.maven.mercury.spi.http.client.FileExchange;
import org.apache.maven.mercury.spi.http.client.HttpClientException;
import org.apache.maven.mercury.spi.http.client.HttpServletResponse;
import org.apache.maven.mercury.spi.http.client.SecureSender;
import org.apache.maven.mercury.spi.http.validate.Validator;
import org.apache.maven.mercury.transport.api.Binding;
import org.apache.maven.mercury.transport.api.Server;
import org.mortbay.jetty.HttpHeaders;
import org.mortbay.jetty.client.HttpExchange;
/**
* RetrievalTarget
* <p/>
* A RetrievalTarget is a remote file that must be downloaded locally, checksummed
* and then atomically moved to its final location. The RetrievalTarget encapsulates
* the temporary local file to which the remote file is downloaded, and also the
* retrieval of the checksum file(s) and the checksum calculation(s).
*/
public abstract class RetrievalTarget
{
private static final IMercuryLogger log = MercuryLoggerManager.getLogger( RetrievalTarget.class );
public static final String __PREFIX = "JTY_";
public static final String __TEMP_SUFFIX = ".tmp";
public static final int __START_STATE = 1;
public static final int __REQUESTED_STATE = 2;
public static final int __READY_STATE = 3;
protected int _checksumState;
protected int _targetState;
protected Server _server;
protected HttpClientException _exception;
protected Binding _binding;
protected File _tempFile;
protected DefaultRetriever _retriever;
protected boolean _complete;
protected HttpExchange _exchange;
protected Set<Validator> _validators;
protected Set<StreamObserver> _observers = new HashSet<StreamObserver>();
protected List<StreamVerifier> _verifiers = new ArrayList<StreamVerifier>();
protected Map<StreamVerifier, String> _verifierMap = new HashMap<StreamVerifier, String>();
public abstract void onComplete();
public abstract void onError( HttpClientException exception );
/**
* Constructor
*
* @param binding
* @param callback
*/
public RetrievalTarget( Server server, DefaultRetriever retriever, Binding binding, Set<Validator> validators, Set<StreamObserver> observers )
{
if ( binding == null ||
(binding.getRemoteResource() == null) ||
(binding.isFile() && (binding.getLocalFile() == null)) ||
(binding.isInMemory() && (binding.getLocalOutputStream() == null)))
{
throw new IllegalArgumentException( "Nothing to retrieve" );
}
_server = server;
_retriever = retriever;
_binding = binding;
_validators = validators;
//sift out the potential checksum verifiers
for (StreamObserver o: observers)
{
if (StreamVerifier.class.isAssignableFrom(o.getClass()))
_verifiers.add((StreamVerifier)o);
else
_observers.add(o);
}
if (_binding.isFile())
{
_tempFile = new File( _binding.getLocalFile().getParentFile(),
__PREFIX + _binding.getLocalFile().getName() + __TEMP_SUFFIX );
_tempFile.deleteOnExit();
if ( !_tempFile.getParentFile().exists() )
{
_tempFile.getParentFile().mkdirs();
}
if ( _tempFile.exists() )
{
onError( new HttpClientException( binding, "File exists " + _tempFile.getAbsolutePath() ) );
}
else if ( !_tempFile.getParentFile().canWrite() )
{
onError( new HttpClientException( binding,
"Unable to write to dir " + _tempFile.getParentFile().getAbsolutePath() ) );
}
}
}
public File getTempFile()
{
return _tempFile;
}
public String getUrl()
{
return _binding.getRemoteResource().toExternalForm();
}
/** Start by getting the appropriate checksums */
public void retrieve()
{
//if there are no checksum verifiers configured, proceed directly to get the file
if (_verifiers.size() == 0)
{
_checksumState = __READY_STATE;
updateTargetState(__START_STATE, null);
}
else
{
_checksumState = __START_STATE;
updateChecksumState(-1, null);
}
}
/** Move the temporary file to its final location */
public boolean move()
{
if (_binding.isFile())
{
boolean ok = _tempFile.renameTo( _binding.getLocalFile() );
if (log.isDebugEnabled())
log.debug("Renaming "+_tempFile.getAbsolutePath()+" to "+_binding.getLocalFile().getAbsolutePath()+": "+ok);
return ok;
}
else
return true;
}
/** Cleanup temp files */
public synchronized void cleanup()
{
deleteTempFile();
if ( _exchange != null )
{
_exchange.cancel();
}
}
public synchronized boolean isComplete()
{
return _complete;
}
public String toString()
{
return "T:" + _binding.getRemoteResource() + ":" + _targetState + ":" + _checksumState + ":" + _complete;
}
private void updateChecksumState (int index, Throwable ex)
{
if ( _exception == null && ex != null )
{
if ( ex instanceof HttpClientException )
{
_exception = (HttpClientException) ex;
}
else
{
_exception = new HttpClientException( _binding, ex );
}
}
if (ex != null)
{
_checksumState = __READY_STATE;
onError(_exception);
}
else
{
boolean proceedWithTargetFile = false;
if (index >= 0)
{
//check if the just-completed retrieval means that we can stop trying to download checksums
StreamVerifier v = _verifiers.get(index);
if (_verifierMap.containsKey(v) && v.getAttributes().isSufficient())
proceedWithTargetFile = true;
}
index++;
if ((index < _verifiers.size()) && !proceedWithTargetFile)
{
retrieveChecksum(index);
}
else
{
_checksumState = __READY_STATE;
//finished retrieving all possible checksums. Add all verifiers
//that had matching checksums into the observers list
_observers.addAll(_verifierMap.keySet());
//now get the file now we have the checksum sorted out
updateTargetState( __START_STATE, null );
}
}
}
/**
* Check the actual checksum against the expected checksum
*
* @return
* @throws StreamVerifierException
*/
public boolean verifyChecksum()
throws StreamVerifierException
{
boolean ok = true;
synchronized (_verifierMap)
{
Iterator<Map.Entry<StreamVerifier, String>> itor = _verifierMap.entrySet().iterator();
while (itor.hasNext() && ok)
{
Map.Entry<StreamVerifier, String> e = itor.next();
ok = e.getKey().verifySignature();
}
}
return ok;
}
public boolean validate( List<String> errors )
{
if ( _validators == null || _validators.isEmpty() )
{
return true;
}
String ext = _binding.getRemoteResource().toString();
if (ext.endsWith("/"))
ext = ext.substring(0, ext.length()-1);
int i = ext.lastIndexOf( "." );
ext = ( i > 0 ? ext.substring( i + 1 ) : "" );
for ( Validator v : _validators )
{
String vExt = v.getFileExtension();
if ( vExt.equalsIgnoreCase( ext ) )
{
try
{
if (_binding.isFile())
{
if ( !v.validate( _tempFile.getCanonicalPath(), errors ) )
{
return false;
}
}
else if (_binding.isInMemory())
{
//TODO Validation on in memory content?
//v.validate(_binding.getInboundContent())
}
}
catch ( IOException e )
{
errors.add( e.getMessage() );
return false;
}
}
}
return true;
}
protected synchronized void updateTargetState( int state, Throwable ex )
{
_targetState = state;
if ( _exception == null && ex != null )
{
if ( ex instanceof HttpClientException )
{
_exception = (HttpClientException) ex;
}
else
{
_exception = new HttpClientException( _binding, ex );
}
}
if ( _targetState == __START_STATE )
{
_exchange = retrieveTargetFile();
}
//if both checksum and target file are ready, we're ready to return callback
else if (_targetState == __READY_STATE )
{
_complete = true;
if ( _exception == null )
{
onComplete();
}
else
{
onError( _exception );
}
}
}
/** Asynchronously fetch the checksum for the target file. */
private HttpExchange retrieveChecksum(final int index)
{
HttpExchange exchange = new HttpExchange.ContentExchange()
{
protected void onException( Throwable ex )
{
//if the checksum is mandatory, then propagate the exception and stop processing
if (!_verifiers.get(index).getAttributes().isLenient())
{
updateChecksumState(index, ex);
}
else
updateChecksumState(index, null);
}
protected void onResponseComplete() throws IOException
{
super.onResponseComplete();
StreamVerifier v = _verifiers.get(index);
if ( getResponseStatus() == HttpServletResponse.SC_OK )
{
//We got a checksum so match it up with the verifier it is for
synchronized (_verifierMap)
{
if( v.getAttributes().isSufficient() )
_verifierMap.clear(); //remove all other entries, we only need one checksum
String actualSignature = getResponseContent().trim();
try
{ // Oleg: verifier need to be loaded upfront
v.initSignature( actualSignature );
}
catch( StreamVerifierException e )
{
throw new IOException(e.getMessage());
}
_verifierMap.put( v, actualSignature );
}
updateChecksumState(index, null);
}
else
{
if (!v.getAttributes().isLenient())
{
//checksum file MUST be present, fail
updateChecksumState(index, new Exception ("Mandatory checksum file not found "+this.getURI()));
}
else
updateChecksumState(index, null);
}
}
};
exchange.setURL( getChecksumFileURLAsString( _verifiers.get(index)) );
try
{
SecureSender.send(_server, _retriever.getHttpClient(), exchange);
}
catch ( Exception ex )
{
updateChecksumState(index, ex);
}
return exchange;
}
/** Asynchronously fetch the target file. */
private HttpExchange retrieveTargetFile()
{
updateTargetState( __REQUESTED_STATE, null );
//get the file, calculating the digest for it on the fly
FileExchange exchange = new FileGetExchange( _server, _binding, getTempFile(), _observers, _retriever.getHttpClient() )
{
public void onFileComplete( String url, File localFile )
{
//we got the target file ok, so tell our main callback
_targetState = __READY_STATE;
updateTargetState( __READY_STATE, null );
}
public void onFileError( String url, Exception e )
{
//an error occurred whilst fetching the file, return an error
_targetState = __READY_STATE;
updateTargetState( __READY_STATE, e );
}
};
if( _server != null && _server.hasUserAgent() )
exchange.setRequestHeader( HttpHeaders.USER_AGENT, _server.getUserAgent() );
exchange.send();
return exchange;
}
private String getChecksumFileURLAsString (StreamVerifier verifier)
{
String extension = verifier.getAttributes().getExtension();
if (extension.charAt(0) != '.')
extension = "."+extension;
return _binding.getRemoteResource().toString() + extension;
}
private boolean deleteTempFile()
{
if ( _tempFile != null && _tempFile.exists() )
{
boolean ok = _tempFile.delete();
if (log.isDebugEnabled())
log.debug("Deleting file "+_tempFile.getAbsolutePath()+" : "+ok);
return ok;
}
return false;
}
}