| /** |
| * 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; |
| } |
| |
| } |