#include "lpathtracker.h"
#include <QDir>
#include <QSet>
#include <QDebug>
LurchPathTracker::LurchPathTracker ()
: QFileSystemWatcher()
{
connect( this, SIGNAL(directoryChanged(QString)), this, SLOT(dirChanged(QString)) );
}
void LurchPathTracker::addWatchedAttribute ( const Lob key )
{
watchKeys << key.copy();
foreach ( QString dir, folders() )
indexDir( dir );
// qDebug() << "watch key added: " << key.toString().trimmed();
// qDebug() << "watch keys now:";
// foreach ( Lob k, watchKeys )
// qDebug() << "\t" << k.toString().trimmed();
}
void LurchPathTracker::removeWatchedAttribute ( const Lob key )
{
int index = -1;
for ( int i = 0 ; i < watchKeys.count() ; i++ ) {
if ( watchKeys[i].equivalentTo( key ) ) {
index = i;
break;
}
}
if ( index == -1 )
return;
watchKeys.removeAt( index );
foreach ( QString filename, fn2attrs.keys() )
fn2attrs[filename].removeAt( index );
}
QList<Lob> LurchPathTracker::watchedAttributes () const
{
QList<Lob> result;
foreach ( Lob L, watchKeys )
result << L.copy();
return result;
}
Lob LurchPathTracker::cachedAttributeFor ( QString urn, Lob keySymbol ) const
{
if ( !urn2fn.contains( urn ) )
return Lob();
QString fn = urn2fn[urn];
if ( !fn2attrs.contains( fn ) )
return Lob();
int index = -1;
for ( int i = 0 ; i < watchKeys.count() ; i++ ) {
if ( watchKeys[i].equivalentTo( keySymbol ) ) {
index = i;
break;
}
}
QList<Lob> values = fn2attrs[fn];
if ( ( index < 0 ) || ( index >= values.count() ) )
return Lob();
return values[index];
}
QMap<QString,Lob> LurchPathTracker::documentsWithAttributes ( Lob keySymbol ) const
{
QMap<QString,Lob> result;
int index = -1;
for ( int i = 0 ; i < watchKeys.count() ; i++ ) {
if ( watchKeys[i].equivalentTo( keySymbol ) ) {
index = i;
break;
}
}
if ( index == -1 )
return result; // empty map
foreach ( QString fn, fn2attrs.keys() ) {
const QList<Lob>& tmp = fn2attrs[fn];
if ( index < tmp.count() ) {
Lob val = tmp[index];
if ( !val.isEmpty() )
result[fn2urn[fn]] = val;
}
}
return result;
}
QString LurchPathTracker::canonical ( const QString path )
{
QFileInfo fi( path );
if ( fi.isRelative() )
fi = QFileInfo( QDir::currentPath() + "/" + path );
QString result = fi.canonicalFilePath();
return ( !fi.exists() || result.isEmpty() ) ? path : result;
}
QStringList LurchPathTracker::add ( QString folder )
{
QString realPath = canonical( folder );
if ( QDir( realPath ).exists() ) { // this ensures it's a directory, not just a file
addPath( realPath ); // adds to this, as a QFileSystemWatcher
return indexDir( realPath );
}
return QStringList(); // no conflicts because it didn't actually process anything
}
void LurchPathTracker::remove ( QString folder )
{
QString realPath = canonical( folder );
dropDir( realPath );
removePath( realPath ); // remove from this, as a QFileSystemWatcher
}
QStringList LurchPathTracker::folders () const
{
return directories();
}
QString LurchPathTracker::URNToFilename ( QString urn ) const
{
return urn2fn.contains( urn ) ? urn2fn[urn] : QString();
}
QString LurchPathTracker::filenameToURN ( QString filename ) const
{
QString realfn = canonical( filename );
return fn2urn.contains( realfn ) ? fn2urn[realfn] : QString();
}
QString LurchPathTracker::URNToNewFilename ( QString urn ) const
{
QStringList urnbits = Lob::splitLurchURN( urn );
if ( urnbits.count() == 0 )
return QString();
QStringList tmp = folders();
QString path = ( tmp.count() > 0 ) ? tmp[0] + "/" : QString();
QString newName = path + urnbits[0] + ".lurch";
unsigned int i = 0;
while ( QFile( newName ).exists() )
newName = path + urnbits[0] + QString( " copy %1" ).arg( ++i ) + ".lurch";
return newName;
}
Lob fastLoad ( QFile& file )
{
// does not process all the XML in the given file, but loads the Lob faster as follows:
// (assumes the file is closed)
// reads lines until one is encountered containing only this:
// <OMS cd="LurchCore" name="document"/>
// once that is read, the content so far is appended with </OMA></OMATTR>,
// and that contentless document is parsed with Lob::fromXML() instead.
// this ignores all document content, but preserves all document attributes,
// useful in the following two routines, which only care about the attributes.
QString xml;
QString line;
QRegExp openDocTag( "\\s*<OMS\\s+(cd=\"LurchCore\"\\s+name=\"document\"|"
"name=\"document\"cd=\"LurchCore\")\\s*/>" );
file.open( QIODevice::ReadOnly );
do {
if ( file.atEnd() ) {
file.close();
return Lob();
}
line = file.readLine();
xml += line;
} while ( openDocTag.indexIn( line ) == -1 );
file.close();
xml += "</OMA></OMATTR>";
return Lob::fromXML( xml ).child();
}
QString LurchPathTracker::loadFileURN ( QString filename ) const
{
if ( !filename.endsWith( ".lurch" ) )
return QString();
QFile infile( filename );
Lob maybeDoc;
maybeDoc.set( fastLoad( infile ) );
return maybeDoc.isDocument() ? maybeDoc.getURN() : QString();
}
QString LurchPathTracker::loadFileURN ( QString filename, QList<Lob>& otherData ) const
{
if ( !filename.endsWith( ".lurch" ) )
return QString();
QFile infile( filename );
Lob maybeDoc;
maybeDoc.set( fastLoad( infile ) );
if ( maybeDoc.isDocument() ) {
for ( int i = 0 ; i < otherData.count() ; i++ ) {
otherData[i] = maybeDoc.attribute( otherData[i] );
otherData[i].setEditable( false );
}
return maybeDoc.getURN();
} else {
otherData.clear();
return QString();
}
}
bool LurchPathTracker::URNAlreadyIndexed ( QString filename, QString& urn )
{
QString realfn = canonical( filename );
if ( urn.isEmpty() )
urn = loadFileURN( realfn );
if ( urn.isEmpty() )
return false;
return urn2fn.contains( urn ) && ( urn2fn[urn] != realfn );
}
bool LurchPathTracker::indexFile ( QString filename, QString urn, QList<Lob> attributeValues )
{
QString realfn = canonical( filename );
QList<Lob> valuesToStore = attributeValues;
if ( urn.isEmpty() || ( attributeValues.count() < watchKeys.count() ) ) {
valuesToStore = watchedAttributes(); // a list of copies of the keys; ok to overwrite
urn = loadFileURN( realfn, valuesToStore );
}
if ( urn.isEmpty() )
return false;
urn2fn[urn] = realfn;
fn2urn[realfn] = urn;
fn2mod[realfn] = QFileInfo( realfn ).lastModified();
fn2attrs[realfn] = valuesToStore;
// qDebug() << realfn;
// qDebug() << "\t"+urn;
// for ( int i = 0 ; i < watchKeys.count() ; i++ )
// if ( !valuesToStore[i].toString().isEmpty()
// && ( watchKeys[i].toString().trimmed()
// != "<OMS cd=\"LurchCore\" name=\"author\"/>" ) )
// qDebug() << watchKeys[i].toString().trimmed()
// << " => " << valuesToStore[i].toString().trimmed();
Q_ASSERT_X( QSet<QString>::fromList( fn2mod.keys() )
== QSet<QString>::fromList( fn2urn.keys() ),
"LurchPathTracker::indexFile", "maps must contain consistent keys" );
Q_ASSERT_X( QSet<QString>::fromList( fn2mod.keys() )
== QSet<QString>::fromList( fn2attrs.keys() ),
"LurchPathTracker::indexFile", "maps must contain consistent keys" );
Q_ASSERT_X( QSet<QString>::fromList( fn2urn.keys() )
== QSet<QString>::fromList( urn2fn.values() ),
"LurchPathTracker::indexFile", "maps must be inverses" );
return true;
}
QStringList LurchPathTracker::indexDir ( QString directory )
{
QString realdir = canonical( directory );
QStringList conflicts;
// for every .lurch file in the directory
foreach ( const QString& file, QDir( realdir ).entryList( QStringList() << "*.lurch" ) ) {
QString urn;
QString realfn = canonical( realdir+"/"+file );
// if its urn has NOT already been indexed
if ( !URNAlreadyIndexed( realfn, urn ) ) {
// then there are no conficts; just index the file we encountered
indexFile( realfn );
} else {
// otherwise there is a conflict, so figure out which file should be indexed
QDateTime lastMod = QFileInfo( realfn ).lastModified();
QString conflict = URNToFilename( urn );
Q_ASSERT_X( fn2mod.contains( conflict ), "LurchPathTracker::indexDir",
"urn2fn map's values must all appear in fn2mod map's keys" );
if ( lastMod > fn2mod[conflict] ) {
// already-indexed version is earlier, so replace its data with the newer data
dropFile( conflict );
indexFile( realfn );
conflicts << conflict;
} else {
// the one we just encountered is earlier, so do not use its data
conflicts << realfn;
}
}
}
return conflicts;
}
bool LurchPathTracker::decreaseConflict ( QString urn )
{
// start by finding the most recently modified (yet unindexed) file with the given URN
QString latestFile;
QDateTime latestTime;
int numFound = 0;
// for every search path
foreach ( const QString& path, folders() ) {
// for every .lurch file in it
foreach ( const QString& file, QDir( path ).entryList( QStringList() << "*.lurch" ) ) {
QString realfn = canonical( path+"/"+file );
QString fileURN = loadFileURN( realfn );
// if it has the right URN, check to see if it's the latest one we've found yet
if ( urn == fileURN ) {
QDateTime thisTime = QFileInfo( realfn ).lastModified();
numFound++;
if ( ( numFound == 0 ) || ( thisTime > latestTime ) ) {
latestFile = realfn;
latestTime = thisTime;
}
}
}
}
// if we found one, index it; either way, report success/failure
if ( !latestFile.isEmpty() ) {
indexFile( latestFile, urn );
emit documentAppeared( urn, numFound > 1 );
return true;
}
return false;
}
void LurchPathTracker::dropFile ( QString filename )
{
QString realfn = canonical( filename );
if ( fn2urn.contains( realfn ) )
urn2fn.remove( fn2urn[realfn] );
fn2urn.remove( realfn );
fn2mod.remove( realfn );
fn2attrs.remove( realfn );
Q_ASSERT_X( QSet<QString>::fromList( fn2mod.keys() )
== QSet<QString>::fromList( fn2urn.keys() ),
"LurchPathTracker::dropFile", "maps must contain consistent keys" );
Q_ASSERT_X( QSet<QString>::fromList( fn2mod.keys() )
== QSet<QString>::fromList( fn2attrs.keys() ),
"LurchPathTracker::dropFile", "maps must contain consistent keys" );
Q_ASSERT_X( QSet<QString>::fromList( fn2urn.keys() )
== QSet<QString>::fromList( urn2fn.values() ),
"LurchPathTracker::dropFile", "maps must be inverses" );
}
void LurchPathTracker::dropDir ( QString directory )
{
QString realdir = canonical( directory );
foreach ( const QString& file, QDir( realdir ).entryList( QStringList() << "*.lurch" ) )
dropFile( canonical( realdir+"/"+file ) );
}
QStringList LurchPathTracker::indexedFiles () const
{
return fn2mod.keys();
}
QStringList LurchPathTracker::indexedURNs () const
{
return urn2fn.keys();
}
void LurchPathTracker::fileDisappeared ( QString file )
{
// this function only gets called if the file was indexed, so I signal its deletion:
QString realfn = canonical( file );
QString urn = fn2urn[realfn];
dropFile( realfn );
emit documentDisappeared( urn );
decreaseConflict( urn );
}
void LurchPathTracker::fileAppeared ( QString file )
{
//qDebug() << "fileAppeared( " << file << ")";
// if not a lurch file, ignore:
if ( !file.endsWith( ".lurch" ) )
return;
// if not a valid document, ignore:
QString realfn = canonical( file );
QFile f( realfn );
Lob maybeDoc;
maybeDoc.set( Lob::fromXML( f ).child() );
if ( !maybeDoc.isDocument() )
return;
// introduces indexing conflict?
QString urn = maybeDoc.getURN();
if ( urn2fn.contains( urn ) && ( urn2fn[urn] != realfn ) ) {
emit documentAppeared( urn, true ); // signal that there was a conflict
} else {
indexFile( realfn, urn ); // index the file for later lookups
emit documentAppeared( urn, false ); // signal new file added ok
}
}
void LurchPathTracker::fileChanged ( QString file )
{
// if file not indexed already, treat it like a file that just appeared...
QString realfn = canonical( file );
if ( !indexedFiles().contains( realfn ) ) {
fileAppeared( realfn );
return;
}
// since it was indexed, it was a valid document before the change, with this urn:
QString oldURN = fn2urn[realfn];
// if the document is no longer valid, remove it from indexing and signal disappearance
QFile f( realfn );
Lob maybeDoc;
maybeDoc.set( Lob::fromXML( f ).child() );
if ( !maybeDoc.isDocument() ) {
dropFile( realfn );
emit documentDisappeared( oldURN );
return;
}
// the document is still valid; proceed as follows
// if no change to urn, just update modification time and signal a generic content change:
QString newURN = maybeDoc.getURN();
if ( oldURN == newURN ) {
fn2mod[oldURN] = QFileInfo( f ).lastModified();
emit documentChanged( oldURN );
return;
}
// there was an urn change; if that creates a conflict, signal it and delete old info:
if ( urn2fn.contains( newURN ) ) { // then it maps it to a different filename, necessarily
dropFile( realfn );
emit documentURNChanged( oldURN, newURN, true );
decreaseConflict( oldURN );
return;
}
// there was an urn change that did not create a conflict; update info and signal it:
dropFile( realfn );
indexFile( realfn, newURN );
emit documentURNChanged( oldURN, newURN, false );
decreaseConflict( oldURN );
}
void LurchPathTracker::dirChanged ( QString dir )
{
QString realdir = canonical( dir );
/*
qDebug() << "dirChanged( " << realdir << ")";
*/
// handle any file disappearances
QStringList formerFiles = indexedFiles();
/*
qDebug() << "\tdirectory was these files: ";
foreach ( const QString& f, formerFiles )
if ( canonical( QFileInfo( f ).path() ) == realdir )
qDebug() << "\t\t" << f.mid( realdir.length() );
*/
foreach ( const QString& file, formerFiles )
if ( ( canonical( QFileInfo( file ).path() ) == realdir ) && !QFile( file ).exists() )
fileDisappeared( file );
// handle any file appearances
QStringList nowFiles = QDir( realdir ).entryList( QStringList() << "*.lurch" );
for ( int i = 0 ; i < nowFiles.count() ; i++ )
nowFiles[i] = realdir+"/"+nowFiles[i];
/*
qDebug() << "\tdirectory is now these files: ";
foreach ( const QString& f, nowFiles )
qDebug() << "\t\t" << f.mid( realdir.length() );
*/
foreach ( const QString& file, nowFiles )
if ( !formerFiles.contains( file ) )
fileAppeared( file );
// handle any file changes
foreach ( const QString& file, formerFiles ) {
QFileInfo fi( file );
if ( fi.exists() && ( fi.lastModified() > fn2mod[file] ) )
fileChanged( file );
}
// notice if directory was deleted
if ( !QDir( dir ).exists() )
removePath( dir );
}