View Javadoc
1   /*
2    * @(#)JarDiffPatcher.java	1.7 05/11/17
3    * 
4    * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved.
5    *
6    * Redistribution and use in source and binary forms, with or without
7    * modification, are permitted provided that the following conditions are met:
8    *
9    * -Redistribution of source code must retain the above copyright notice, this
10   *  list of conditions and the following disclaimer.
11   *
12   * -Redistribution in binary form must reproduce the above copyright notice,
13   *  this list of conditions and the following disclaimer in the documentation
14   *  and/or other materials provided with the distribution.
15   *
16   * Neither the name of Sun Microsystems, Inc. or the names of contributors may
17   * be used to endorse or promote products derived from this software without
18   * specific prior written permission.
19   *
20   * This software is provided "AS IS," without a warranty of any kind. ALL
21   * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
22   * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
23   * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN")
24   * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
25   * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
26   * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
27   * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
28   * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
29   * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
30   * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
31   *
32   * You acknowledge that this software is not designed, licensed or intended
33   * for use in the design, construction, operation or maintenance of any
34   * nuclear facility.
35   */
36  
37  package jnlp.sample.jardiff;
38  
39  import java.io.File;
40  import java.io.IOException;
41  import java.io.InputStream;
42  import java.io.InputStreamReader;
43  import java.io.LineNumberReader;
44  import java.io.OutputStream;
45  import java.util.ArrayList;
46  import java.util.Enumeration;
47  import java.util.HashMap;
48  import java.util.HashSet;
49  import java.util.List;
50  import java.util.Map;
51  import java.util.MissingResourceException;
52  import java.util.ResourceBundle;
53  import java.util.Set;
54  import java.util.jar.JarEntry;
55  import java.util.jar.JarFile;
56  import java.util.jar.JarOutputStream;
57  import java.util.zip.ZipEntry;
58  
59  /**
60   * JarDiff is able to create a jar file containing the delta between two
61   * jar files (old and new). The delta jar file can then be applied to the
62   * old jar file to reconstruct the new jar file.
63   * <p/>
64   * Refer to the JNLP spec for details on how this is done.
65   *
66   * @version 1.11, 06/26/03
67   */
68  public class JarDiffPatcher
69      implements JarDiffConstants, Patcher
70  {
71      private static final int DEFAULT_READ_SIZE = 2048;
72  
73      private static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
74  
75      private static byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
76  
77      private static ResourceBundle _resources = JarDiff.getResources();
78  
79      public static ResourceBundle getResources()
80      {
81          return JarDiff.getResources();
82      }
83  
84      public void applyPatch( Patcher.PatchDelegate delegate, String oldJarPath, String jarDiffPath, OutputStream result )
85          throws IOException
86      {
87          File oldFile = new File( oldJarPath );
88          File diffFile = new File( jarDiffPath );
89          JarOutputStream jos = new JarOutputStream( result );
90          JarFile oldJar = new JarFile( oldFile );
91          JarFile jarDiff = new JarFile( diffFile );
92          Set<String> ignoreSet = new HashSet<String>();
93          Map<String, String> renameMap = new HashMap<String, String>();
94  
95          determineNameMapping( jarDiff, ignoreSet, renameMap );
96  
97          // get all keys in renameMap
98          String[] keys = renameMap.keySet().toArray( new String[renameMap.size()] );
99  
100         // Files to implicit move
101         Set<String> oldjarNames = new HashSet<String>();
102 
103         Enumeration<JarEntry> oldEntries = oldJar.entries();
104         if ( oldEntries != null )
105         {
106             while ( oldEntries.hasMoreElements() )
107             {
108                 oldjarNames.add( ( oldEntries.nextElement() ).getName() );
109             }
110         }
111 
112         // size depends on the three parameters below, which is
113         // basically the counter for each loop that do the actual
114         // writes to the output file
115         // since oldjarNames.size() changes in the first two loop
116         // below, we need to adjust the size accordingly also when
117         // oldjarNames.size() changes
118         double size = oldjarNames.size() + keys.length + jarDiff.size();
119         double currentEntry = 0;
120 
121         // Handle all remove commands
122         oldjarNames.removeAll( ignoreSet );
123         size -= ignoreSet.size();
124 
125         // Add content from JARDiff
126         Enumeration<JarEntry> entries = jarDiff.entries();
127         if ( entries != null )
128         {
129             while ( entries.hasMoreElements() )
130             {
131                 JarEntry entry = entries.nextElement();
132 
133                 if ( !INDEX_NAME.equals( entry.getName() ) )
134                 {
135 
136                     updateDelegate( delegate, currentEntry, size );
137                     currentEntry++;
138 
139                     writeEntry( jos, entry, jarDiff );
140 
141                     // Remove entry from oldjarNames since no implicit
142                     //move is needed
143                     boolean wasInOld = oldjarNames.remove( entry.getName() );
144 
145                     // Update progress counters. If it was in old, we do
146                     // not need an implicit move, so adjust total size.
147                     if ( wasInOld )
148                     {
149                         size--;
150                     }
151 
152                 }
153                 else
154                 {
155                     // no write is done, decrement size
156                     size--;
157                 }
158             }
159         }
160 
161         // go through the renameMap and apply move for each entry
162         for ( String newName : keys )
163         {
164 
165             // Apply move <oldName> <newName> command
166             String oldName = renameMap.get( newName );
167 
168             // Get source JarEntry
169             JarEntry oldEntry = oldJar.getJarEntry( oldName );
170 
171             if ( oldEntry == null )
172             {
173                 String moveCmd = MOVE_COMMAND + oldName + " " + newName;
174                 handleException( "jardiff.error.badmove", moveCmd );
175             }
176 
177             // Create dest JarEntry
178             JarEntry newEntry = new JarEntry( newName );
179             newEntry.setTime( oldEntry.getTime() );
180             newEntry.setSize( oldEntry.getSize() );
181             newEntry.setCompressedSize( oldEntry.getCompressedSize() );
182             newEntry.setCrc( oldEntry.getCrc() );
183             newEntry.setMethod( oldEntry.getMethod() );
184             newEntry.setExtra( oldEntry.getExtra() );
185             newEntry.setComment( oldEntry.getComment() );
186 
187             updateDelegate( delegate, currentEntry, size );
188             currentEntry++;
189 
190             writeEntry( jos, newEntry, oldJar.getInputStream( oldEntry ) );
191 
192             // Remove entry from oldjarNames since no implicit
193             //move is needed
194             boolean wasInOld = oldjarNames.remove( oldName );
195 
196             // Update progress counters. If it was in old, we do
197             // not need an implicit move, so adjust total size.
198             if ( wasInOld )
199             {
200                 size--;
201             }
202 
203         }
204 
205         // implicit move
206         for ( String name : oldjarNames )
207         {
208             JarEntry entry = oldJar.getJarEntry( name );
209 
210             updateDelegate( delegate, currentEntry, size );
211             currentEntry++;
212 
213             writeEntry( jos, entry, oldJar );
214         }
215 
216         updateDelegate( delegate, currentEntry, size );
217 
218         jos.finish();
219     }
220 
221     private void updateDelegate( Patcher.PatchDelegate delegate, double currentSize, double size )
222     {
223         if ( delegate != null )
224         {
225             delegate.patching( (int) ( currentSize / size ) );
226         }
227     }
228 
229     private void determineNameMapping( JarFile jarDiff, Set<String> ignoreSet, Map<String, String> renameMap )
230         throws IOException
231     {
232         InputStream is = jarDiff.getInputStream( jarDiff.getEntry( INDEX_NAME ) );
233 
234         if ( is == null )
235         {
236             handleException( "jardiff.error.noindex", null );
237         }
238         LineNumberReader indexReader = new LineNumberReader( new InputStreamReader( is, "UTF-8" ) );
239         String line = indexReader.readLine();
240 
241         if ( line == null || !line.equals( VERSION_HEADER ) )
242         {
243             handleException( "jardiff.error.badheader", line );
244         }
245 
246         while ( ( line = indexReader.readLine() ) != null )
247         {
248             if ( line.startsWith( REMOVE_COMMAND ) )
249             {
250                 List<String> sub = getSubpaths( line.substring( REMOVE_COMMAND.length() ) );
251 
252                 if ( sub.size() != 1 )
253                 {
254                     handleException( "jardiff.error.badremove", line );
255                 }
256                 ignoreSet.add( sub.get( 0 ) );
257             }
258             else if ( line.startsWith( MOVE_COMMAND ) )
259             {
260                 List<String> sub = getSubpaths( line.substring( MOVE_COMMAND.length() ) );
261 
262                 if ( sub.size() != 2 )
263                 {
264                     handleException( "jardiff.error.badmove", line );
265                 }
266                 // target of move should be the key
267                 if ( renameMap.put( sub.get( 1 ), sub.get( 0 ) ) != null )
268                 {
269                     // invalid move - should not move to same target twice
270                     handleException( "jardiff.error.badmove", line );
271                 }
272             }
273             else if ( line.length() > 0 )
274             {
275                 handleException( "jardiff.error.badcommand", line );
276             }
277         }
278     }
279 
280     private void handleException( String errorMsg, String line )
281         throws IOException
282     {
283         try
284         {
285             throw new IOException( getResources().getString( errorMsg ) + " " + line );
286         }
287         catch ( MissingResourceException mre )
288         {
289             System.err.println( "Fatal error: " + errorMsg );
290             new Throwable().printStackTrace( System.err );
291             System.exit( -1 );
292         }
293     }
294 
295     private List<String> getSubpaths( String path )
296     {
297         int index = 0;
298         int length = path.length();
299         List<String> sub = new ArrayList<String>();
300 
301         while ( index < length )
302         {
303             while ( index < length && Character.isWhitespace( path.charAt( index ) ) )
304             {
305                 index++;
306             }
307             if ( index < length )
308             {
309                 int start = index;
310                 int last = start;
311                 String subString = null;
312 
313                 while ( index < length )
314                 {
315                     char aChar = path.charAt( index );
316                     if ( aChar == '\\' && ( index + 1 ) < length && path.charAt( index + 1 ) == ' ' )
317                     {
318 
319                         if ( subString == null )
320                         {
321                             subString = path.substring( last, index );
322                         }
323                         else
324                         {
325                             subString += path.substring( last, index );
326                         }
327                         last = ++index;
328                     }
329                     else if ( Character.isWhitespace( aChar ) )
330                     {
331                         break;
332                     }
333                     index++;
334                 }
335                 if ( last != index )
336                 {
337                     if ( subString == null )
338                     {
339                         subString = path.substring( last, index );
340                     }
341                     else
342                     {
343                         subString += path.substring( last, index );
344                     }
345                 }
346                 sub.add( subString );
347             }
348         }
349         return sub;
350     }
351 
352     private void writeEntry( JarOutputStream jos, JarEntry entry, JarFile file )
353         throws IOException
354     {
355         writeEntry( jos, entry, file.getInputStream( entry ) );
356     }
357 
358     private void writeEntry( JarOutputStream jos, JarEntry entry, InputStream data )
359         throws IOException
360     {
361         //Create a new ZipEntry to clear the compressed size. 5079423
362         jos.putNextEntry( new ZipEntry( entry.getName() ) );
363 
364         // Read the entry
365         int size = data.read( newBytes );
366 
367         while ( size != -1 )
368         {
369             jos.write( newBytes, 0, size );
370             size = data.read( newBytes );
371         }
372         data.close();
373     }
374 }
375 
376