package textbender.a.r.page.navdo; // Copyright 2007, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Textbender Software"), to deal in the Textbender Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Textbender Software, and to permit persons to whom the Textbender 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 Textbender Software. THE TEXTBENDER 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 TEXTBENDER SOFTWARE OR THE USE OR OTHER DEALINGS IN THE TEXTBENDER SOFTWARE. import java.io.*; import java.net.*; import java.util.regex.*; import textbender.a.r.page.*; import textbender.g.io.*; import textbender.g.lang.*; import textbender.g.util.logging.*; import textbender.o.*; import textbender.o.rhinohide.*; /** A modified version of an original working file. * Its filename is a mangled form of the original. * Its location is the same directory, * so relative links to other files are unbroken. *
* Each modification of the original creates a new series of versioned files. * Each modification of a versioned file creates a new versioned file in the same series. * For example, here is an original file, and a single series: *
** myfile.xht * .myfile_navdo-0v0.xht * .myfile_navdo-0v1.xht * .myfile_navdo-0v2.xht * ~ ~ ~ **
* Versioned filenames follow the general pattern: *
** .BODY_navdo-SvV(DOTX)? **
* where: * * . (dot) prefix is actually ~ (tilde) on Windows * BODY is {@linkplain #originalNameBody originalNameBody} * S is {@linkplain #series series} * V is {@linkplain #version version} * DOTX is {@linkplain #originalNameDotExtension originalNameDotExtension} **
* Version 1 is the initial modification of the series. * Version 0 is the baseline copy of the original. * It is created at the same time as version 1. * It serves to store the original file contents, * in case the original file is subsequently overwritten * (by saving one of the modified versions, for example). *
** Re-modifying the original file — by reloading it * in the browser, for example — results in a new series. * In this case, the navigation history might look something like: *
** myfile.xht * .myfile_navdo-0v0.xht * .myfile_navdo-0v1.xht * .myfile_navdo-0v2.xht * ~ ~ ~ * myfile.xht * .myfile_navdo-1v0.xht * .myfile_navdo-1v1.xht * .myfile_navdo-1v2.xht * ~ ~ ~ **
* Re-modifying a versioned file — by reloading it * in the browser, for example — results in a new versioned file. * New versioned files are always given new numbers; * old ones are never overwritten. * All versioned files are deleted when the VM exits. *
** New versioned files are created using {@linkplain #after(File) after}. * It is important to make use of {@linkplain #initRedirect(RhiWindow) initRedirect}, * as well *
*/ public @ThreadSafe class VersionedFile extends File { /** Constructs a VersionedFile. * Physically creates the empty file too, as a side-effect. * Usage: ** The physical file is deleted either at VM exit, * or sooner in the case of a transient {@linkplain NewSv1File new-Sv1} file. *
* * @param oldFile prior to modification * * @see #initRedirect(RhiWindow) */ public static VersionedFile after( File oldFile ) throws IOException { VersionedFile versionedFile; Matcher m = NAME_PATTERN.matcher( oldFile.getName() ); if( m.matches() ) { final String originalNameBody = m.group( 1 ); final int series = Integer.parseInt( m.group( 2 )); int version = Integer.parseInt( m.group( 3 )) + 1; String originalNameDotExtension = m.group( 4 ); if( originalNameDotExtension == null ) originalNameDotExtension = ""; final File originalFile = new File ( oldFile.getParentFile(), originalNameBody + originalNameDotExtension ); findUnusedMod: for( ;; ++version ) // Atomic test/create. Could use File.createTempFile() instead, but it might silently truncate parts the name, making them unparseable. { versionedFile = new VersionedFile ( originalFile, series, originalNameBody, DISCRIMINATOR, version, originalNameDotExtension ); if( versionedFile.createNewFile() ) break; } } else // oldFile is not itself a versioned file { m = FileX.BODY_DOTX_PATTERN.matcher( oldFile.getName() ); final String originalNameBody; final String originalNameDotExtension; if( m.matches() ) { originalNameBody = m.group( 1 ); originalNameDotExtension = m.group( 2 ); } else { assert false; originalNameBody = oldFile.getName(); originalNameDotExtension = ""; } final File originalFile = new File ( oldFile.getParentFile(), originalNameBody + originalNameDotExtension ); VersionedFile v0; findUnusedSeries: for( int series = 0;; ++series ) { v0 = new VersionedFile ( originalFile, series, originalNameBody, DISCRIMINATOR, /*version*/0, originalNameDotExtension ); if( v0.createNewFile() ) break; // atomic test/create } FileX.copyAs( v0, originalFile ); // v0.setLastModified( originalFile.lastModified() ); versionedFile = new NewSv1File ( originalFile, v0.series, originalNameBody, originalNameDotExtension ); } return versionedFile; } /** Returns the file as a VersionedFile; * or null if it is not actually a versioned file. */ public static VersionedFile fromFile( File file ) { Matcher m = NAME_PATTERN.matcher( file.getName() ); if( !m.matches() ) return null; final String originalNameBody = m.group( 1 ); final int series = Integer.parseInt( m.group( 2 )); int version = Integer.parseInt( m.group( 3 )); String originalNameDotExtension = m.group( 4 ); if( originalNameDotExtension == null ) originalNameDotExtension = ""; final File originalFile = new File ( file.getParentFile(), originalNameBody + originalNameDotExtension ); return new VersionedFile ( originalFile, series, originalNameBody, DISCRIMINATOR, version, originalNameDotExtension ); } /** Constructs a VersionedFile. */ VersionedFile( File originalFile, int series, String originalNameBody, String discriminator, int version, String originalNameDotExtension ){ super ( originalFile.getParentFile(), // "." + originalNameBody + "_" + discriminator + Integer.toString(series) NAME_PREFIX + originalNameBody + "_" + discriminator + Integer.toString(series) + "v" + Integer.toString(version) + originalNameDotExtension ); this.originalFile = originalFile; this.series = series; this.originalNameBody = originalNameBody; this.version = version; this.originalNameDotExtension = originalNameDotExtension; deleteOnExit(); // if this is a transient NewSv1File, it'll often be renamed before then [but not always]; no harm } // ```````````````````````````````````````````````````````````````````````````````````` // Initialized early, for use in other initializers. private static final String NAME_PREFIX; static { if( OperatingSystem.isWindows() ) NAME_PREFIX = "~"; // Because dot files cannot be renamed on Windows. Actually, the the renaming problem may have been due to something else. But dot files behave strangely on Windows, so this is better. // if( Run.i().isOSWindows() ) NAME_PREFIX = "~"; // Because dot files cannot be renamed on Windows. Actually, the the renaming problem may have been due to something else. But dot files behave strangely on Windows, so this is better. else NAME_PREFIX = "."; // hides the file, on Unix } // ------------------------------------------------------------------------------------ /** If the current page source is local, returns it as a file. ** In future, this method might download a remote source * to a new local file. Currently, it does not. * Currently, only local files are supported. *
* * @throws UnsupportedOperationException if the current page is remote */ public static File getOrCreateDocumentAsModifiableFile( PageVisit pageVisit )// throws IOException { if( pageVisit.file() != null ) return pageVisit.file(); // File serializedPageFile = pageVisit.serializedPage(); // File userDirectory = new File( System.getProperty( "user.dir" )); // FIX, ought to be "user.home"? // File newUserFile = File.createTempFile // ( /*prefix*/"new", /*suffix*/".xht", userDirectory ); // FIX parse out the URI's file name as prefix, and extension as suffix // FileX.copyAs( newUserFile, serializedPageFile ); // return newUserFile; ////// But serialization will not result in clean file (per PageVisit.serializedPage). // Would download, but no clients require it. // Problem is, remote files are {@linkplain PageVisit#isPageSilent() silent}, // and tools that intiate modification cannot easily grapple with them. // So in practice, remote pages are not modifiable on the fly: throw new UnsupportedOperationException( "modification from remote page" ); } /** If this is a new series, initializes it * and redirects the browser to the first versioned file. * Otherwise does nothing. * This method ought to be called for every newly loaded page. * * @return true if redirection was performed; false otherwise */ public final boolean initRedirect( RhiWindow window ) throws IOException { if( version != 0 ) return false; final NewSv1File newV1 = new NewSv1File ( originalFile, series, originalNameBody, originalNameDotExtension ); // if( !newV1.isFile() || newV1.length() == 0L ) return false; // or same thing: if( newV1.length() == 0L ) return false; // initial redirection from v0 already done, and user is merely re-visiting it; so we know because newV1 is renamed or deleted, or deletion was attempted (see below) VersionedFile v1 = new VersionedFile ( originalFile, series, originalNameBody, DISCRIMINATOR, /*version*/1, originalNameDotExtension ); // if( !FileX.renameFrom( newV1, v1 )) throw new IOException( "failed to rename '" + newV1 + "' to '" + v1 + "'" ); //// fails often, http://reluk.ca/system/host/tinman/windows-update-2007-03.xht#firefox if( FileX.renameFromDefaultsToCopy(newV1,v1) && !newV1.delete() ) { LoggerX.i(FileX.class).config( "delete failed, working around it: " + newV1 ); new FileOutputStream( newV1 ).close(); // deleting content instead, so newV1.length signals attempt to delete (used above) } window.eval ( "location.href = '" + v1.toURI().toASCIIString() + "' + location.search + location.hash" // same ?query#fragment as current page ); return true; } /** Loads the newly created versioned file into the browser, * for initial viewing. Sets the page visit * {@linkplain PageVisit#getReleaseReason() release reason} to * {@linkplain #loadAsNewVersion_releaseReason loadAsNewVersion_releaseReason}. * Takes care of any multi-step load requirements, involving redirection. * (For these reasons, use this method rather than loading the file by name.) */ public final void loadAsNewVersion( final PageVisit pageVisit ) { final VersionedFile fileToLoad; if( VersionedFile.this instanceof NewSv1File ) { fileToLoad = new VersionedFile ( originalFile, series, originalNameBody, DISCRIMINATOR, /*version*/0, originalNameDotExtension ); } else fileToLoad = VersionedFile.this; pageVisit.setReleaseReason( loadAsNewVersion_releaseReason ); pageVisit.window().eval ( "location.href = '" + fileToLoad.toURI().toASCIIString() + "' + location.search + location.hash" // same ?query#fragment as current page ); } /** @see #loadAsNewVersion(PageVisit) */ public static final Object loadAsNewVersion_releaseReason = "loading newly created versioned file"; /** Matches the simple (end-of-path) name of a versioned file. * Does not match transient {@linkplain NewSv1File new-Sv1} files, however. */ public static final Pattern NAME_PATTERN = Pattern.compile // ( "^\\.(.+?)_navdo-([0-9]+)v([0-9]+)(\\.[^.]*)?$" ); ( "^" + Pattern.quote(NAME_PREFIX) + "(.+?)_navdo-([0-9]+)v([0-9]+)(\\.[^.]*)?$" ); /** Original file of the series, typically the user's working document. */ public final File originalFile() { return originalFile; } private final File originalFile; /** Saves this versioned file's contents to the original file. * In future, this method will implement an overwrite guard, * to avoid clobbering file content that was generated by another process; * but currently it does not. */ public final void saveToOriginal() throws IOException { FileX.copyAs( originalFile, VersionedFile.this ); // originalFile.setLastModified( VersionedFile.this.lastModified() ); } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** Unique discriminator in the name of each versioned file. */ static final String DISCRIMINATOR = "navdo-"; /** Body part of the simple name of the original file. * * @see FileX#BODY_DOTX_PATTERN */ final String originalNameBody; /** Dot-extension of original file. May be empty. * * @see FileX#BODY_DOTX_PATTERN */ final String originalNameDotExtension; /** Series number of this versioned file. */ final int series; /** Version number of this versioned file. */ final int version; }