001    /**
002     * Copyright 2008-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.codehaus.mojo.wagon.shared;
017    
018    /*
019     * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
020     * agreements. See the NOTICE file distributed with this work for additional information regarding
021     * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
022     * "License"); you may not use this file except in compliance with the License. You may obtain a
023     * copy of the License at
024     * 
025     * http://www.apache.org/licenses/LICENSE-2.0
026     * 
027     * Unless required by applicable law or agreed to in writing, software distributed under the License
028     * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
029     * or implied. See the License for the specific language governing permissions and limitations under
030     * the License.
031     */
032    
033    import java.io.File;
034    import java.io.FileInputStream;
035    import java.io.FileReader;
036    import java.io.FileWriter;
037    import java.io.IOException;
038    import java.io.InputStream;
039    import java.io.Reader;
040    import java.io.Writer;
041    import java.security.MessageDigest;
042    import java.security.NoSuchAlgorithmException;
043    
044    import org.apache.maven.artifact.repository.metadata.Metadata;
045    import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
046    import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer;
047    import org.apache.maven.plugin.logging.Log;
048    import org.apache.maven.shared.model.fileset.FileSet;
049    import org.apache.maven.wagon.ResourceDoesNotExistException;
050    import org.apache.maven.wagon.Wagon;
051    import org.apache.maven.wagon.WagonException;
052    import org.codehaus.plexus.util.DirectoryScanner;
053    import org.codehaus.plexus.util.FileUtils;
054    import org.codehaus.plexus.util.IOUtil;
055    import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
056    
057    /**
058     * A copy of stage's plugin RepositoryCopier but use WagonUpload and WagonDownload instead
059     * 
060     * @plexus.component role="org.codehaus.mojo.wagon.shared.MavenRepoMerger" role-hint="default"
061     */
062    
063    public class DefaultMavenRepoMerger
064        implements MavenRepoMerger
065    {
066        /**
067         * @plexus.requirement role="org.codehaus.mojo.wagon.shared.WagonDownload"
068         */
069        private WagonDownload downloader;
070    
071        /**
072         * @plexus.requirement role="org.codehaus.mojo.wagon.shared.WagonUpload"
073         */
074        private WagonUpload uploader;
075    
076        public void merge( Wagon src, Wagon target, boolean optimize, Log logger )
077            throws WagonException, IOException
078        {
079    
080            // copy src to a local dir
081            File downloadSrcDir =  createTempDirectory( "wagon-maven-plugin" );
082    
083            WagonFileSet srcFileSet = new WagonFileSet();
084            srcFileSet.setDownloadDirectory( downloadSrcDir );
085            // ignore archiva/nexus .index at root dir
086            String[] excludes = { ".index/**", ".indexer/**, .meta/**, .nexus/**" };
087            srcFileSet.setExcludes( excludes );
088    
089            try
090            {
091                downloader.download( src, srcFileSet, logger );
092    
093                // merge metadata
094                DirectoryScanner scanner = new DirectoryScanner();
095                scanner.setBasedir( downloadSrcDir );
096                String[] includes = { "**/" + MAVEN_METADATA };
097                scanner.setIncludes( includes );
098                scanner.scan();
099                String[] files = scanner.getIncludedFiles();
100    
101                for ( int i = 0; i < files.length; ++i )
102                {
103                    File srcMetadaFile = new File( downloadSrcDir, files[i] + IN_PROCESS_MARKER );
104    
105                    try
106                    {
107                        target.get( files[i].replace( '\\', '/' ), srcMetadaFile );
108                    }
109                    catch ( ResourceDoesNotExistException e )
110                    {
111                        // We don't have an equivalent on the targetRepositoryUrl side because we have something
112                        // new on the sourceRepositoryUrl side so just skip the metadata merging.
113                        continue;
114                    }
115    
116                    try
117                    {
118                        mergeMetadata( srcMetadaFile, logger );
119                    }
120                    catch ( XmlPullParserException e )
121                    {
122                        throw new IOException( "Metadata file is corrupt " + files[i] + " Reason: " + e.getMessage() );
123                    }
124    
125                }
126    
127                // upload to target
128                FileSet tobeUploadedFileSet = new FileSet();
129                tobeUploadedFileSet.setDirectory( downloadSrcDir.getAbsolutePath() );
130    
131                this.uploader.upload( target, tobeUploadedFileSet, optimize, logger );
132    
133            }
134            finally
135            {
136                FileUtils.deleteDirectory( downloadSrcDir );
137            }
138    
139        }
140    
141        private void mergeMetadata( File existingMetadata, Log logger )
142            throws IOException, XmlPullParserException
143        {
144    
145            Writer stagedMetadataWriter = null;
146            Reader existingMetadataReader = null;
147            Reader stagedMetadataReader = null;
148            File stagedMetadataFile = null;
149    
150            try
151            {
152                MetadataXpp3Reader xppReader = new MetadataXpp3Reader();
153                MetadataXpp3Writer xppWriter = new MetadataXpp3Writer();
154    
155                // Existing Metadata in target stage
156                existingMetadataReader = new FileReader( existingMetadata );
157                Metadata existing = xppReader.read( existingMetadataReader );
158    
159                // Staged Metadata
160                stagedMetadataFile = new File( existingMetadata.getParentFile(), MAVEN_METADATA );
161                stagedMetadataReader = new FileReader( stagedMetadataFile );
162                Metadata staged = xppReader.read( stagedMetadataReader );
163    
164                // Merge and write back to staged metadata to replace the remote one
165                existing.merge( staged );
166    
167                stagedMetadataWriter = new FileWriter( stagedMetadataFile );
168                xppWriter.write( stagedMetadataWriter, existing );
169    
170                logger.info( "Merging metadata file: " + stagedMetadataFile );
171    
172            }
173            finally
174            {
175                IOUtil.close( stagedMetadataWriter );
176                IOUtil.close( stagedMetadataReader );
177                IOUtil.close( existingMetadataReader );
178    
179                existingMetadata.delete();
180            }
181    
182            // Mark all metadata as in-process and regenerate the checksums as they will be different
183            // after the merger
184    
185            try
186            {
187                File newMd5 = new File( stagedMetadataFile.getParentFile(), MAVEN_METADATA + ".md5" );
188                FileUtils.fileWrite( newMd5.getAbsolutePath(), checksum( stagedMetadataFile, MD5 ) );
189    
190                File newSha1 = new File( stagedMetadataFile.getParentFile(), MAVEN_METADATA + ".sha1" );
191                FileUtils.fileWrite( newSha1.getAbsolutePath(), checksum( stagedMetadataFile, SHA1 ) );
192            }
193            catch ( NoSuchAlgorithmException e )
194            {
195                throw new RuntimeException( e );
196            }
197    
198            // We have the new merged copy so we're good
199    
200        }
201    
202        private String checksum( File file, String type )
203            throws IOException, NoSuchAlgorithmException
204        {
205            MessageDigest md5 = MessageDigest.getInstance( type );
206    
207            InputStream is = new FileInputStream( file );
208    
209            byte[] buf = new byte[8192];
210    
211            int i;
212    
213            while ( ( i = is.read( buf ) ) > 0 )
214            {
215                md5.update( buf, 0, i );
216            }
217    
218            IOUtil.close( is );
219    
220            return encode( md5.digest() );
221        }
222    
223        private String encode( byte[] binaryData )
224        {
225            if ( binaryData.length != 16 && binaryData.length != 20 )
226            {
227                int bitLength = binaryData.length * 8;
228                throw new IllegalArgumentException( "Unrecognised length for binary data: " + bitLength + " bits" );
229            }
230    
231            String retValue = "";
232    
233            for ( int i = 0; i < binaryData.length; i++ )
234            {
235                String t = Integer.toHexString( binaryData[i] & 0xff );
236    
237                if ( t.length() == 1 )
238                {
239                    retValue += ( "0" + t );
240                }
241                else
242                {
243                    retValue += t;
244                }
245            }
246    
247            return retValue.trim();
248        }
249    
250        public static File createTempDirectory(String prefix)
251            throws IOException
252        {
253            final File temp;
254    
255            temp = File.createTempFile( prefix, Long.toString( System.nanoTime() ) );
256    
257            if ( !( temp.delete() ) )
258            {
259                throw new IOException( "Could not delete temp file: " + temp.getAbsolutePath() );
260            }
261    
262            if ( !( temp.mkdir() ) )
263            {
264                throw new IOException( "Could not create temp directory: " + temp.getAbsolutePath() );
265            }
266    
267            return ( temp );
268        }
269    
270    }