package votorola.a.diff; // Copyright 2010-2013, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. import java.io.*; import java.net.*; import java.util.*; import java.util.regex.*; import votorola.a.*; import votorola.a.position.*; import votorola.g.hold.*; import votorola.g.io.*; import votorola.g.lang.*; import votorola.g.logging.*; /** A directory of cached diff output ~/votorola/in/diff. Cache files are * created with broad permissions for writing by all (vote-server account, Tomcat and * other process owners). Administrators should avoid deleting files at runtime, or * temporary process errors may occur. * * @see en.wikipedia.org/wiki/Diff * @see www.gnu.org/software/diffutils/manual/diff.html */ public final @ThreadSafe class DiffCache extends File { /** Constructs a DiffCache for inclusion in a vote-server. * * @see VoteServer#diffCache() */ public DiffCache( VoteServer _vS, final VoteServer.ConstructionContext cc ) throws IOException { super( _vS.inDirectory(), "diff" ); vS = _vS; if( cc == null ) throw new NullPointerException(); // cc is unused except to preclude redundant construction external to vote-server if( !exists() ) { if( !mkdir() ) throw new IOException( "unable to create directory: " + DiffCache.this ); setWritable( true, /*ownerOnly*/false ); } } // ------------------------------------------------------------------------------------ /** Returns the cached diff output for the specified draft pair. If the output is not * yet cached, then it is generated anew and cached for future calls. */ public File diffFile( final DraftPair pair ) throws IOException { final File diffFile = new File( DiffCache.this, pair.diffKeyParse().key() ); if( !diffFile.isFile() ) { final DraftRevision aDraft = pair.aCore().draft(); final DraftRevision bDraft = pair.bCore().draft(); createDiffFile( diffFile, aDraft.wikiScriptURI(), aDraft.rev(), bDraft.wikiScriptURI(), bDraft.rev() ); } return diffFile; } /** The line transformer (LT) for the diffs of this wiki cache. It partly determines * the structure of each diff (D) and is equally tied to the diff identification * scheme (ID-D-LT). ID maps a diff URL (and cache filename) to a D in a stable * manner, such that a reader following a URL that was embedded in an archived * discussion will see the same D as the original conversants. Any change of * transformer (LT') that would substantially alter a diff (D') will therefore be * deployed under a new identification scheme (ID'). */ public static final LineTransformer1 LINE_TRANSFORMER = new LineTransformer1(); /** The pattern of a negative 'diff' result, where the two files are identical. */ public static final Pattern NO_DIFF_PATTERN = Pattern.compile( "^Files .+ and .+ are identical$" ); // cf. s.gwt.mediawiki.DifferenceShadowsV.NO_DIFF_PATTERN //// P r i v a t e /////////////////////////////////////////////////////////////////////// private File createDiffFile( final File diffFile, final URI aWikiScriptLocation, final int aRev, final URI bWikiScriptLocation, final int bRev ) throws IOException { final Spool fileSpool = new Spool1(); final File aFile = LINE_TRANSFORMER.fetchPageAsFile( aWikiScriptLocation, "oldid", aRev, "DiffCache_a", fileSpool )[1]; final File bFile = LINE_TRANSFORMER.fetchPageAsFile( bWikiScriptLocation, "oldid", bRev, "DiffCache_b", fileSpool )[1]; final ProcessBuilder pB = new ProcessBuilder( "/bin/sh", "-c", "diff --report-identical-files --unified '" + aFile.getName() + "' '" + bFile.getName() + "'" ); pB.directory( aFile.getParentFile() ); LoggerX.i(getClass()).config( "from directory " + pB.directory() + ", calling out to OS: " + pB.command() ); final File diffTmpFile = File.createTempFile( "DiffCache", "." + diffFile.getName() ); // try // { final Process p = pB.start(); final InputStream in = new BufferedInputStream( p.getInputStream() ); try { final OutputStream out = new BufferedOutputStream( new FileOutputStream( diffTmpFile )); try { for( ;; ) { int bb = in.read(); if( bb == -1 ) break; out.write( bb ); } } finally{ out.close(); } } finally{ in.close(); } final int exitValue = ProcessX.waitForWithoutInterrupt( p ); if( exitValue < /*no difference*/0 || exitValue > /*difference*/1 ) throw new IOException( "exit value of " + exitValue + " from process: " + pB.command() ); fileSpool.unwind(); // if no exceptions, otherwise keep files for admin to diagnose diffTmpFile.setWritable( true, /*ownerOnly*/false ); // before moving to cache // if( !FileX.renameFrom( diffTmpFile, diffFile )) throw new IOException( "unable to rename file: " + diffTmpFile + " -> " + diffFile ); /// but mysteriously failing for my voff, though all permissions seem correct // FileX.renameFromDefaultsToCopy( diffTmpFile, diffFile ); /// Java copy does not allow for preservation of the writable permissions as set above FileX.renameFromDefaultToMv( diffTmpFile, diffFile ); // } // finally{ if( diffTmpFile.isFile() ) diffTmpFile.delete(); } // clean up from exception or failure return diffFile; } /** Returns the cached diff output for the specified difference key. If the output is * not yet cached, then it is generated anew and cached for future calls. Generation * may entail constructing a draft pair; if you already have one constructed, then * use {@linkplain #diffFile(DraftPair) diffFile}(pair) instead. * * @see DiffKey */ private File diffFile( final String key ) throws IOException { final File diffFile = new File( DiffCache.this, key ); if( !diffFile.isFile() ) { final DiffKeyParse parse = new DiffKeyParse( key ); final List aPath = parse.aPath(); final List bPath = parse.bPath(); final int aRev; final int bRev; final URI aWikiScriptLocation; final URI bWikiScriptLocation; if( aPath.size() == 1 && bPath.size() == 1 ) { aRev = aPath.get( 0 ); bRev = bPath.get( 0 ); aWikiScriptLocation = bWikiScriptLocation = vS.pollwiki().scriptURI(); } else { final DraftPair pair = DraftPair.newDraftPair( parse, vS.pollwiki() ); final DraftRevision aDraft = pair.aCore().draft(); final DraftRevision bDraft = pair.bCore().draft(); aRev = aDraft.rev(); bRev = bDraft.rev(); aWikiScriptLocation = aDraft.wikiScriptURI(); bWikiScriptLocation = bDraft.wikiScriptURI(); } createDiffFile( diffFile, aWikiScriptLocation, aRev, bWikiScriptLocation, bRev ); } return diffFile; } private final VoteServer vS; }