001package 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.
002
003import java.io.*;
004import java.net.*;
005import java.util.*;
006import java.util.regex.*;
007import votorola.a.*;
008import votorola.a.position.*;
009import votorola.g.hold.*;
010import votorola.g.io.*;
011import votorola.g.lang.*;
012import votorola.g.logging.*;
013
014
015/** A directory of cached diff output <code>~/votorola/in/diff</code>.  Cache files are
016  * created with broad permissions for writing by all (vote-server account, Tomcat and
017  * other process owners).  Administrators should avoid deleting files at runtime, or
018  * temporary process errors may occur.
019  *
020  *     @see <a href='http://en.wikipedia.org/wiki/Diff'
021  *                         >en.wikipedia.org/wiki/Diff</a>
022  *     @see <a href='http://www.gnu.org/software/diffutils/manual/diff.html'
023  *                         >www.gnu.org/software/diffutils/manual/diff.html</a>
024  */
025public final @ThreadSafe class DiffCache extends File
026{
027
028
029    /** Constructs a DiffCache for inclusion in a vote-server.
030      *
031      *     @see VoteServer#diffCache()
032      */
033    public DiffCache( VoteServer _vS, final VoteServer.ConstructionContext cc ) throws IOException
034    {
035        super( _vS.inDirectory(), "diff" );
036        vS = _vS;
037        if( cc == null ) throw new NullPointerException();
038          // cc is unused except to preclude redundant construction external to vote-server
039
040        if( !exists() )
041        {
042            if( !mkdir() ) throw new IOException( "unable to create directory: " + DiffCache.this );
043
044            setWritable( true, /*ownerOnly*/false );
045        }
046    }
047
048
049
050   // ------------------------------------------------------------------------------------
051
052
053    /** Returns the cached diff output for the specified draft pair.  If the output is not
054      * yet cached, then it is generated anew and cached for future calls.
055      */
056    public File diffFile( final DraftPair pair ) throws IOException
057    {
058        final File diffFile = new File( DiffCache.this, pair.diffKeyParse().key() );
059        if( !diffFile.isFile() )
060        {
061            final DraftRevision aDraft = pair.aCore().draft();
062            final DraftRevision bDraft = pair.bCore().draft();
063            createDiffFile( diffFile, aDraft.wikiScriptURI(), aDraft.rev(),
064                                      bDraft.wikiScriptURI(), bDraft.rev() );
065        }
066        return diffFile;
067    }
068
069
070
071    /** The line transformer (LT) for the diffs of this wiki cache.  It partly determines
072      * the structure of each diff (D) and is equally tied to the diff identification
073      * scheme (ID-D-LT).  ID maps a diff URL (and cache filename) to a D in a stable
074      * manner, such that a reader following a URL that was embedded in an archived
075      * discussion will see the same D as the original conversants.  Any change of
076      * transformer (LT') that would substantially alter a diff (D') will therefore be
077      * deployed under a new identification scheme (ID').
078      */
079    public static final LineTransformer1 LINE_TRANSFORMER = new LineTransformer1();
080
081
082
083    /** The pattern of a negative 'diff' result, where the two files are identical.
084      */
085    public static final Pattern NO_DIFF_PATTERN = Pattern.compile(
086      "^Files .+ and .+ are identical$" ); // cf. s.gwt.mediawiki.DifferenceShadowsV.NO_DIFF_PATTERN
087
088
089
090//// P r i v a t e ///////////////////////////////////////////////////////////////////////
091
092
093    private File createDiffFile( final File diffFile,
094      final URI aWikiScriptLocation, final int aRev,
095      final URI bWikiScriptLocation, final int bRev ) throws IOException
096    {
097        final Spool fileSpool = new Spool1();
098        final File aFile = LINE_TRANSFORMER.fetchPageAsFile( aWikiScriptLocation, "oldid",
099          aRev, "DiffCache_a", fileSpool )[1];
100        final File bFile = LINE_TRANSFORMER.fetchPageAsFile( bWikiScriptLocation, "oldid",
101          bRev, "DiffCache_b", fileSpool )[1];
102        final ProcessBuilder pB = new ProcessBuilder(
103          "/bin/sh", "-c", "diff --report-identical-files --unified '" + aFile.getName() + "' '"
104          + bFile.getName() + "'" );
105        pB.directory( aFile.getParentFile() );
106        LoggerX.i(getClass()).config( "from directory " + pB.directory() + ", calling out to OS: " + pB.command() );
107
108        final File diffTmpFile = File.createTempFile( "DiffCache", "." + diffFile.getName() );
109     // try
110     // {
111            final Process p = pB.start();
112            final InputStream in = new BufferedInputStream( p.getInputStream() );
113            try
114            {
115                final OutputStream out = new BufferedOutputStream( new FileOutputStream(
116                  diffTmpFile ));
117                try
118                {
119                    for( ;; ) { int bb = in.read(); if( bb == -1 ) break; out.write( bb ); }
120                }
121                finally{ out.close(); }
122            }
123            finally{ in.close(); }
124
125            final int exitValue = ProcessX.waitForWithoutInterrupt( p );
126            if( exitValue < /*no difference*/0 || exitValue > /*difference*/1 ) throw new IOException( "exit value of " + exitValue + " from process: " + pB.command() );
127
128            fileSpool.unwind(); // if no exceptions, otherwise keep files for admin to diagnose
129            diffTmpFile.setWritable( true, /*ownerOnly*/false ); // before moving to cache
130         // if( !FileX.renameFrom( diffTmpFile, diffFile )) throw new IOException( "unable to rename file: " + diffTmpFile + " -> " + diffFile );
131         /// but mysteriously failing for my voff, though all permissions seem correct
132         // FileX.renameFromDefaultsToCopy( diffTmpFile, diffFile );
133         /// Java copy does not allow for preservation of the writable permissions as set above
134            FileX.renameFromDefaultToMv( diffTmpFile, diffFile );
135     // }
136     // finally{ if( diffTmpFile.isFile() ) diffTmpFile.delete(); } // clean up from exception or failure
137        return diffFile;
138    }
139
140
141
142    /** Returns the cached diff output for the specified difference key.  If the output is
143      * not yet cached, then it is generated anew and cached for future calls.  Generation
144      * may entail constructing a draft pair; if you already have one constructed, then
145      * use {@linkplain #diffFile(DraftPair) diffFile}(pair) instead.
146      *
147      *     @see DiffKey
148      */
149    private File diffFile( final String key ) throws IOException
150    {
151        final File diffFile = new File( DiffCache.this, key );
152        if( !diffFile.isFile() )
153        {
154            final DiffKeyParse parse = new DiffKeyParse( key );
155            final List<Integer> aPath = parse.aPath();
156            final List<Integer> bPath = parse.bPath();
157            final int aRev;
158            final int bRev;
159            final URI aWikiScriptLocation;
160            final URI bWikiScriptLocation;
161            if( aPath.size() == 1 && bPath.size() == 1 )
162            {
163                aRev = aPath.get( 0 );
164                bRev = bPath.get( 0 );
165                aWikiScriptLocation = bWikiScriptLocation = vS.pollwiki().scriptURI();
166            }
167            else
168            {
169                final DraftPair pair = DraftPair.newDraftPair( parse, vS.pollwiki() );
170                final DraftRevision aDraft = pair.aCore().draft();
171                final DraftRevision bDraft = pair.bCore().draft();
172                aRev = aDraft.rev();
173                bRev = bDraft.rev();
174                aWikiScriptLocation = aDraft.wikiScriptURI();
175                bWikiScriptLocation = bDraft.wikiScriptURI();
176            }
177            createDiffFile( diffFile, aWikiScriptLocation, aRev, bWikiScriptLocation, bRev );
178        }
179        return diffFile;
180    }
181
182
183
184    private final VoteServer vS;
185
186
187}