1102 lines
29 KiB
Perl
1102 lines
29 KiB
Perl
|
/* $Id$
|
||
|
|
||
|
Part of SWI-Prolog
|
||
|
|
||
|
Author: Jan Wielemaker
|
||
|
E-mail: wielemak@science.uva.nl
|
||
|
WWW: http://www.swi-prolog.org
|
||
|
Copyright (C): 1985-2007, University of Amsterdam
|
||
|
|
||
|
This program is free software; you can redistribute it and/or
|
||
|
modify it under the terms of the GNU General Public License
|
||
|
as published by the Free Software Foundation; either version 2
|
||
|
of the License, or (at your option) any later version.
|
||
|
|
||
|
This program is distributed in the hope that it will be useful,
|
||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
GNU General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU Lesser General Public
|
||
|
License along with this library; if not, write to the Free Software
|
||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||
|
|
||
|
As a special exception, if you link this library with other files,
|
||
|
compiled with a Free Software compiler, to produce an executable, this
|
||
|
library does not by itself cause the resulting executable to be covered
|
||
|
by the GNU General Public License. This exception does not however
|
||
|
invalidate any other reasons why the executable file might be covered by
|
||
|
the GNU General Public License.
|
||
|
*/
|
||
|
|
||
|
|
||
|
:- module(rdf_edit,
|
||
|
[ rdfe_assert/3, % Sub, Pred, Obj
|
||
|
rdfe_assert/4, % Sub, Pred, Obj, PayLoad
|
||
|
rdfe_retractall/3, % Sub, Pred, Obj
|
||
|
rdfe_retractall/4, % Sub, Pred, Obj, PayLoad
|
||
|
rdfe_update/4, % Sub, Pred, Obj, +Action
|
||
|
rdfe_update/5, % Sub, Pred, Obj, +PayLoad, +Action
|
||
|
rdfe_load/1, % +File
|
||
|
rdfe_load/2, % +File, +Options
|
||
|
rdfe_delete/1, % +Resource
|
||
|
|
||
|
rdfe_register_ns/2, % +Id, +URI
|
||
|
rdfe_unregister_ns/2, % +Id, +URI
|
||
|
|
||
|
rdfe_reset/0, % clear everything
|
||
|
|
||
|
rdfe_transaction/1, % :Goal
|
||
|
rdfe_transaction/2, % :Goal, +Name
|
||
|
rdfe_transaction_member/2, % +Transactions, -Action
|
||
|
rdfe_transaction_name/2, % +Transactions, -Name
|
||
|
rdfe_set_transaction_name/1,% +Name
|
||
|
|
||
|
rdfe_set_watermark/1, % +Name
|
||
|
|
||
|
rdfe_undo/0, %
|
||
|
rdfe_redo/0,
|
||
|
rdfe_can_undo/1, % -TID
|
||
|
rdfe_can_redo/1, % -TID
|
||
|
|
||
|
rdfe_set_file_property/2, % +File, +Property
|
||
|
rdfe_get_file_property/2, % ?File, ?Property
|
||
|
|
||
|
rdfe_is_modified/1, % ?File
|
||
|
rdfe_clear_modified/1, % +File
|
||
|
|
||
|
rdfe_open_journal/2, % +File, +Mode
|
||
|
rdfe_close_journal/0,
|
||
|
rdfe_replay_journal/1, % +File
|
||
|
rdfe_current_journal/1, % -Path
|
||
|
|
||
|
rdfe_snapshot_file/1 % -File
|
||
|
]).
|
||
|
:- use_module(rdf_db).
|
||
|
:- use_module(library(broadcast)).
|
||
|
:- use_module(library(lists)).
|
||
|
:- use_module(library(debug)).
|
||
|
:- use_module(library(url)).
|
||
|
|
||
|
:- meta_predicate
|
||
|
rdfe_transaction(:),
|
||
|
rdfe_transaction(:, +).
|
||
|
|
||
|
:- dynamic
|
||
|
undo_log/5, % TID, Action, Subj, Pred, Obj
|
||
|
current_transaction/1, % TID
|
||
|
transaction_name/2, % TID, Name
|
||
|
undo_marker/2, % Mode, TID
|
||
|
journal/3, % Path, Mode, Stream
|
||
|
unmodified_md5/2, % Path, MD5
|
||
|
snapshot_file/1. % File
|
||
|
|
||
|
/** <module> RDF edit layer
|
||
|
This library provides a number of functions on top of the rdf_db module:
|
||
|
|
||
|
* Broadcast modifications
|
||
|
* Provide undo/redo
|
||
|
|
||
|
@tbd This library must be rewritten using rdf_monitor/3. This allows
|
||
|
using edit layer without having to choose between rdf_ and rdfe_
|
||
|
predicates.
|
||
|
|
||
|
@see rdf_persistency.pl provides reliable persistency, but without
|
||
|
changes boardcasting and undo/redo.
|
||
|
*/
|
||
|
|
||
|
:- rdf_meta
|
||
|
rdfe_assert(r,r,o),
|
||
|
rdfe_assert(r,r,o,+),
|
||
|
rdfe_retractall(r,r,o),
|
||
|
rdfe_update(r,r,o,t),
|
||
|
rdfe_delete(r),
|
||
|
rdfe_transaction(:),
|
||
|
rdfe_transaction(:, +).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* BASIC EDIT OPERATIONS *
|
||
|
*******************************/
|
||
|
|
||
|
rdfe_assert(Subject, Predicate, Object) :-
|
||
|
rdfe_assert(Subject, Predicate, Object, user).
|
||
|
|
||
|
rdfe_assert(Subject, Predicate, Object, PayLoad) :-
|
||
|
rdf_assert(Subject, Predicate, Object, PayLoad),
|
||
|
rdfe_current_transaction(TID),
|
||
|
assert_action(TID, assert(PayLoad), Subject, Predicate, Object),
|
||
|
journal(assert(TID, Subject, Predicate, Object, PayLoad)).
|
||
|
|
||
|
rdfe_retractall(Subject, Predicate, Object) :-
|
||
|
rdfe_retractall(Subject, Predicate, Object, _).
|
||
|
|
||
|
rdfe_retractall(Subject, Predicate, Object, PayLoad) :-
|
||
|
rdfe_current_transaction(TID),
|
||
|
( rdf(Subject, Predicate, Object, PayLoad),
|
||
|
assert_action(TID, retract(PayLoad), Subject, Predicate, Object),
|
||
|
journal(retract(TID, Subject, Predicate, Object, PayLoad)),
|
||
|
fail
|
||
|
; true
|
||
|
),
|
||
|
rdf_retractall(Subject, Predicate, Object, PayLoad).
|
||
|
|
||
|
%% rdfe_update(+Subject, +Predicate, +Object, +Action)
|
||
|
%
|
||
|
% Update an existing triple. Possible actions are:
|
||
|
%
|
||
|
%% subject(+Subject)
|
||
|
%% predicate(+Predicate)
|
||
|
%% object(+Object)
|
||
|
%% source(+Source)
|
||
|
|
||
|
rdfe_update(Subject, Predicate, Object, Action) :-
|
||
|
rdfe_current_transaction(TID),
|
||
|
rdf_update(Subject, Predicate, Object, Action),
|
||
|
( Action = object(New)
|
||
|
-> assert_action(TID, object(Object), Subject, Predicate, New)
|
||
|
; Action = predicate(New)
|
||
|
-> assert_action(TID, predicate(Predicate), Subject, New, Object)
|
||
|
; Action = subject(New)
|
||
|
-> assert_action(TID, subject(Subject), New, Predicate, Object)
|
||
|
; Action = source(New)
|
||
|
-> forall(rdf(Subject, Predicate, Object, PayLoad),
|
||
|
assert_action(TID, source(PayLoad, New),
|
||
|
Subject, Predicate, Object))
|
||
|
),
|
||
|
journal(update(TID, Subject, Predicate, Object, Action)).
|
||
|
|
||
|
rdfe_update(Subject, Predicate, Object, PayLoad, Action) :-
|
||
|
rdfe_current_transaction(TID),
|
||
|
rdf_update(Subject, Predicate, Object, PayLoad, Action),
|
||
|
( Action = source(New)
|
||
|
-> assert_action(TID, source(PayLoad, New),
|
||
|
Subject, Predicate, Object)
|
||
|
; throw(tbd) % source is used internally
|
||
|
),
|
||
|
journal(update(TID, Subject, Predicate, Object, PayLoad, Action)).
|
||
|
|
||
|
%% rdfe_delete(+Subject)
|
||
|
%
|
||
|
% Delete a subject and all we know about it. This is a bit tricky.
|
||
|
% If we are involved in transitive relations, should we re-joint
|
||
|
% these in this module?
|
||
|
|
||
|
rdfe_delete(Subject) :-
|
||
|
rdfe_transaction(delete(Subject)).
|
||
|
|
||
|
delete(Subject) :-
|
||
|
rdfe_retractall(Subject, _, _),
|
||
|
rdfe_retractall(_, Subject, _),
|
||
|
rdfe_retractall(_, _, Subject).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* FILE HANDLING *
|
||
|
*******************************/
|
||
|
|
||
|
%% rdfe_load(+File) is det.
|
||
|
%% rdfe_load(+File, +Options) is det.
|
||
|
%
|
||
|
% Load an RDF file and record this action including version information
|
||
|
% to facilitate reliable reload.
|
||
|
|
||
|
rdfe_load(File) :-
|
||
|
rdfe_load(File, []).
|
||
|
|
||
|
|
||
|
rdfe_load(File, Options) :-
|
||
|
rdfe_current_transaction(TID),
|
||
|
absolute_file_name(File,
|
||
|
[ access(read),
|
||
|
extensions([rdf,rdfs,owl,''])
|
||
|
], Path),
|
||
|
rdf_load(Path,
|
||
|
[ graph(Graph),
|
||
|
modified(Modified)
|
||
|
| Options
|
||
|
]),
|
||
|
( Modified == not_modified
|
||
|
-> true
|
||
|
; absolute_file_name('.', PWD),
|
||
|
size_file(Path, Size),
|
||
|
( Modified = last_modified(Stamp)
|
||
|
-> true
|
||
|
; time_file(Path, Stamp)
|
||
|
),
|
||
|
SecTime is round(Stamp),
|
||
|
rdf_statistics(triples_by_file(Graph, Triples)),
|
||
|
rdf_md5(Graph, MD5),
|
||
|
assert_action(TID, load_file(Path), -, -, -),
|
||
|
journal(rdf_load(TID,
|
||
|
Path,
|
||
|
[ pwd(PWD),
|
||
|
size(Size),
|
||
|
modified(SecTime),
|
||
|
triples(Triples),
|
||
|
md5(MD5),
|
||
|
from(File)
|
||
|
])),
|
||
|
ensure_snapshot(Path)
|
||
|
).
|
||
|
|
||
|
|
||
|
rdfe_unload(Path) :-
|
||
|
rdfe_current_transaction(TID),
|
||
|
rdf_unload(Path),
|
||
|
assert_action(TID, unload_file(Path), -, -, -),
|
||
|
journal(rdf_unload(TID, Path)).
|
||
|
|
||
|
|
||
|
%% ensure_snapshot(+Path)
|
||
|
%
|
||
|
% Ensure we have a snapshot of Path if we are making a journal, so
|
||
|
% we can always reload the snapshot to ensure exactly the same
|
||
|
% state.
|
||
|
|
||
|
ensure_snapshot(Path) :-
|
||
|
rdfe_current_journal(_),
|
||
|
rdf_md5(Path, MD5),
|
||
|
( snapshot_file(Path, MD5,
|
||
|
[ access(read),
|
||
|
file_errors(fail)
|
||
|
],
|
||
|
File)
|
||
|
-> debug(snapshot, 'Existing snapshot for ~w on ~w', [Path, File])
|
||
|
; snapshot_file(Path, MD5,
|
||
|
[ access(write)
|
||
|
],
|
||
|
File),
|
||
|
debug(snapshot, 'Saving snapshot for ~w to ~w', [Path, File]),
|
||
|
rdf_save_db(File, Path)
|
||
|
),
|
||
|
assert(snapshot_file(File)).
|
||
|
ensure_snapshot(_).
|
||
|
|
||
|
|
||
|
%% load_snapshot(+Source, +Path)
|
||
|
%
|
||
|
% Load triples from the given snapshot file. One of the troubles
|
||
|
% is the time-stamp to avoid rdf_make/0 from reloading the file.
|
||
|
% for the time being we use 1e12, which is a lot further in the
|
||
|
% future than this system is going to live.
|
||
|
|
||
|
load_snapshot(Source, Path) :-
|
||
|
statistics(cputime, T0),
|
||
|
rdf_load_db(Path),
|
||
|
statistics(cputime, T1),
|
||
|
Time is T1 - T0,
|
||
|
rdf_statistics(triples_by_file(Source, Triples)),
|
||
|
rdf_md5(Source, MD5),
|
||
|
% 1e10: modified far in the future
|
||
|
assert(rdf_db:rdf_source(Source, 1e12, Triples, MD5)),
|
||
|
print_message(informational,
|
||
|
rdf(loaded(Source, Triples, snapshot(Time)))),
|
||
|
assert(snapshot_file(Path)).
|
||
|
|
||
|
|
||
|
%% snapshot_file(+Path, +MD5, +Access, -File)
|
||
|
%
|
||
|
% Find existing snapsnot file or location to save a new one.
|
||
|
|
||
|
snapshot_file(Path, MD5, Options, SnapShot) :-
|
||
|
file_base_name(Path, Base),
|
||
|
atomic_list_concat([Base, @, MD5], File),
|
||
|
absolute_file_name(snapshot(File),
|
||
|
[ extensions([trp])
|
||
|
| Options
|
||
|
],
|
||
|
SnapShot).
|
||
|
|
||
|
|
||
|
%% rdfe_snapshot_file(-File)
|
||
|
%
|
||
|
% Enumerate the MD5 snapshot files required to restore the current
|
||
|
% journal file. Using this call we can write a routine that
|
||
|
% packages the journal file with all required snapshots to restore
|
||
|
% the journal on another computer.
|
||
|
|
||
|
rdfe_snapshot_file(File) :-
|
||
|
snapshot_file(File).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* NAMESPACE HANDLING *
|
||
|
*******************************/
|
||
|
|
||
|
:- dynamic
|
||
|
system_ns/2.
|
||
|
:- volatile
|
||
|
system_ns/2.
|
||
|
|
||
|
%% rdfe_register_ns(Id, URI)
|
||
|
%
|
||
|
% Encapsulation of rdf_register_ns(Id, URI)
|
||
|
|
||
|
rdfe_register_ns(Id, URI) :-
|
||
|
rdf_db:ns(Id, URI), !.
|
||
|
rdfe_register_ns(Id, URI) :-
|
||
|
save_system_ns,
|
||
|
rdfe_current_transaction(TID),
|
||
|
rdf_register_ns(Id, URI),
|
||
|
broadcast(rdf_ns(register(Id, URI))),
|
||
|
assert_action(TID, ns(register(Id, URI)), -, -, -),
|
||
|
journal(ns(TID, register(Id, URI))).
|
||
|
|
||
|
rdfe_unregister_ns(Id, URI) :-
|
||
|
save_system_ns,
|
||
|
rdfe_current_transaction(TID),
|
||
|
retractall(rdf_db:ns(Id, URI)),
|
||
|
broadcast(rdf_ns(unregister(Id, URI))),
|
||
|
assert_action(TID, ns(unregister(Id, URI)), -, -, -),
|
||
|
journal(ns(TID, unregister(Id, URI))).
|
||
|
|
||
|
% rdfe_register_ns/0
|
||
|
%
|
||
|
% Reset namespaces to the state they where before usage of the
|
||
|
% rdf_edit layer.
|
||
|
|
||
|
rdfe_reset_ns :-
|
||
|
( system_ns(_, _)
|
||
|
-> retractall(rdf_db:ns(Id, URI)),
|
||
|
forall(system_ns(Id, URI), assert(rdb_db:ns(Id, URI)))
|
||
|
; true
|
||
|
).
|
||
|
|
||
|
save_system_ns :-
|
||
|
system_ns(_, _), !. % already done
|
||
|
save_system_ns :-
|
||
|
forall(rdf_db:ns(Id, URI), assert(system_ns(Id, URI))).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* TRANSACTIONS *
|
||
|
*******************************/
|
||
|
|
||
|
%% rdfe_transaction(:Goal)
|
||
|
%
|
||
|
% Run Goal, recording all modifications as a single transaction.
|
||
|
% If Goal raises an exception or fails, all changes are
|
||
|
% rolled-back.
|
||
|
|
||
|
rdfe_transaction(Goal) :-
|
||
|
rdfe_transaction(Goal, []).
|
||
|
rdfe_transaction(Goal, Name) :-
|
||
|
rdfe_begin_transaction(Name),
|
||
|
( catch(Goal, E, true)
|
||
|
-> ( var(E)
|
||
|
-> check_file_protection(Error),
|
||
|
( var(Error)
|
||
|
-> rdfe_commit
|
||
|
; rdfe_rollback,
|
||
|
throw(Error)
|
||
|
)
|
||
|
; rdfe_rollback,
|
||
|
throw(E)
|
||
|
)
|
||
|
; rdfe_rollback,
|
||
|
fail
|
||
|
).
|
||
|
|
||
|
%% rdfe_begin_transaction
|
||
|
%
|
||
|
% Start a transaction. This is followed by either rdfe_end_transaction
|
||
|
% or rdfe_rollback. Transactions may be nested.
|
||
|
|
||
|
rdfe_begin_transaction(Name) :-
|
||
|
current_transaction(TID), !, % nested transaction
|
||
|
append(TID, [1], TID2),
|
||
|
asserta(current_transaction(TID2)),
|
||
|
assert(transaction_name(TID2, Name)).
|
||
|
rdfe_begin_transaction(Name) :- % toplevel transaction
|
||
|
flag(rdf_edit_tid, TID, TID+1),
|
||
|
asserta(current_transaction([TID])),
|
||
|
assert(transaction_name(TID, Name)).
|
||
|
|
||
|
rdfe_current_transaction(TID) :-
|
||
|
current_transaction(TID), !.
|
||
|
rdfe_current_transaction(_) :-
|
||
|
throw(error(existence_error(rdf_transaction, _), _)).
|
||
|
|
||
|
rdfe_commit :-
|
||
|
retract(current_transaction(TID)), !,
|
||
|
retractall(undo_marker(_, _)),
|
||
|
( rdfe_transaction_member(TID, _)
|
||
|
-> get_time(Time), % transaction is not empty
|
||
|
journal(commit(TID, Time)),
|
||
|
( TID = [Id]
|
||
|
-> broadcast(rdf_transaction(Id))
|
||
|
; true
|
||
|
)
|
||
|
; true
|
||
|
).
|
||
|
|
||
|
rdfe_rollback :-
|
||
|
retract(current_transaction(TID)), !,
|
||
|
journal(rollback(TID)),
|
||
|
rollback(TID).
|
||
|
|
||
|
%% rollback(+TID)
|
||
|
%
|
||
|
% This is the same as undo/1, but it must not record the undone
|
||
|
% actions as rollbacks cannot be `redone'. Somehow there should
|
||
|
% be a cleaner way to distinguish between transactional operations
|
||
|
% and plain operations.
|
||
|
|
||
|
rollback(TID) :-
|
||
|
append(TID, _, Id),
|
||
|
( retract(undo_log(Id, Action, Subject, Predicate, Object)),
|
||
|
( rollback(Action, Subject, Predicate, Object)
|
||
|
-> fail
|
||
|
; print_message(error,
|
||
|
rdf_undo_failed(undo(Action, Subject,
|
||
|
Predicate, Object))),
|
||
|
fail
|
||
|
)
|
||
|
; true
|
||
|
).
|
||
|
|
||
|
rollback(assert(PayLoad), Subject, Predicate, Object) :- !,
|
||
|
rdf_retractall(Subject, Predicate, Object, PayLoad).
|
||
|
rollback(retract(PayLoad), Subject, Predicate, Object) :- !,
|
||
|
rdf_assert(Subject, Predicate, Object, PayLoad).
|
||
|
rollback(Action, Subject, Predicate, Object) :-
|
||
|
action(Action), !,
|
||
|
rdf_update(Subject, Predicate, Object, Action).
|
||
|
|
||
|
|
||
|
assert_action(TID, Action, Subject, Predicate, Object) :-
|
||
|
asserta(undo_log(TID, Action, Subject, Predicate, Object)).
|
||
|
|
||
|
%% undo(+TID)
|
||
|
%
|
||
|
% Undo a transaction as well as possible transactions nested into
|
||
|
% it.
|
||
|
|
||
|
undo(TID) :-
|
||
|
append(TID, _, Id),
|
||
|
( retract(undo_log(Id, Action, Subject, Predicate, Object)),
|
||
|
( undo(Action, Subject, Predicate, Object)
|
||
|
-> fail
|
||
|
; print_message(warning,
|
||
|
rdf_undo_failed(undo(Action, Subject,
|
||
|
Predicate, Object))),
|
||
|
fail
|
||
|
)
|
||
|
; true
|
||
|
).
|
||
|
|
||
|
undo(assert(PayLoad), Subject, Predicate, Object) :- !,
|
||
|
rdfe_retractall(Subject, Predicate, Object, PayLoad).
|
||
|
undo(retract(PayLoad), Subject, Predicate, Object) :- !,
|
||
|
rdfe_assert(Subject, Predicate, Object, PayLoad).
|
||
|
undo(source(Old, New), Subject, Predicate, Object) :- !,
|
||
|
rdfe_update(Subject, Predicate, Object, Old, source(New)).
|
||
|
undo(ns(Action), -, -, -) :- !,
|
||
|
( Action = register(Id, URI)
|
||
|
-> rdfe_unregister_ns(Id, URI)
|
||
|
; Action = unregister(Id, URI)
|
||
|
-> rdfe_register_ns(Id, URI)
|
||
|
).
|
||
|
undo(load_file(Path), -, -, -) :- !,
|
||
|
rdfe_unload(Path).
|
||
|
undo(unload_file(Path), -, -, -) :- !,
|
||
|
rdfe_load(Path).
|
||
|
undo(Action, Subject, Predicate, Object) :-
|
||
|
action(Action), !,
|
||
|
rdfe_update(Subject, Predicate, Object, Action).
|
||
|
|
||
|
action(subject(_)).
|
||
|
action(predicate(_)).
|
||
|
action(object(_)).
|
||
|
|
||
|
%% rdfe_undo
|
||
|
%
|
||
|
% Undo a (toplevel) transaction. More calls do further undo. The
|
||
|
% `Undone' actions are re-added to the undo log, so the user can
|
||
|
% redo them. Fails if there are no more undo/redo transactions.
|
||
|
|
||
|
rdfe_undo :-
|
||
|
undo_marker(undo, TID), !,
|
||
|
( undo_previous(TID, UnDone)
|
||
|
-> retractall(undo_marker(_, _)),
|
||
|
assert(undo_marker(undo, UnDone)),
|
||
|
broadcast(rdf_undo(undo, UnDone))
|
||
|
; fail % start of undo log
|
||
|
).
|
||
|
rdfe_undo :-
|
||
|
retract(undo_marker(redo, _)), !,
|
||
|
last_transaction(TID),
|
||
|
undo_previous(TID, UnDone),
|
||
|
assert(undo_marker(undo, UnDone)),
|
||
|
broadcast(rdf_undo(undo, UnDone)).
|
||
|
rdfe_undo :-
|
||
|
last_transaction(TID),
|
||
|
undo_previous(TID, UnDone),
|
||
|
assert(undo_marker(undo, UnDone)),
|
||
|
broadcast(rdf_undo(undo, UnDone)).
|
||
|
|
||
|
find_previous_undo(-1, _) :- !,
|
||
|
fail.
|
||
|
find_previous_undo(TID, TID) :-
|
||
|
undo_log([TID|_], _, _, _, _), !.
|
||
|
find_previous_undo(TID0, TID) :-
|
||
|
TID1 is TID0 - 1,
|
||
|
find_previous_undo(TID1, TID).
|
||
|
|
||
|
undo_previous(TID, Undone) :-
|
||
|
find_previous_undo(TID, Undone),
|
||
|
rdfe_transaction(undo([Undone])).
|
||
|
|
||
|
last_transaction(TID) :-
|
||
|
undo_log([TID|_], _, _, _, _), !.
|
||
|
|
||
|
%% rdfe_redo
|
||
|
%
|
||
|
% Start a redo-session
|
||
|
|
||
|
rdfe_redo :-
|
||
|
( retract(undo_marker(undo, _))
|
||
|
-> last_transaction(TID),
|
||
|
undo_previous(TID, UnDone),
|
||
|
assert(undo_marker(redo, UnDone))
|
||
|
; retract(undo_marker(redo, TID))
|
||
|
-> undo_previous(TID, UnDone),
|
||
|
assert(undo_marker(redo, UnDone))
|
||
|
; true
|
||
|
),
|
||
|
broadcast(rdf_undo(redo, UnDone)).
|
||
|
|
||
|
|
||
|
%% rdfe_can_redo(-TID) is semidet.
|
||
|
%% rdfe_can_undo(-TID) is semidet.
|
||
|
%
|
||
|
% Check if we can undo and if so return the id of the transaction
|
||
|
% that will be un/re-done. A subsequent call to rdfe_transaction_name
|
||
|
% can be used to give a hint in the UI.
|
||
|
|
||
|
rdfe_can_redo(Redo) :-
|
||
|
undo_marker(undo, _), !,
|
||
|
last_transaction(TID),
|
||
|
find_previous_undo(TID, Redo).
|
||
|
rdfe_can_redo(Redo) :-
|
||
|
undo_marker(redo, TID),
|
||
|
find_previous_undo(TID, Redo).
|
||
|
|
||
|
rdfe_can_undo(Undo) :- % continue undo
|
||
|
undo_marker(undo, TID), !,
|
||
|
find_previous_undo(TID, Undo).
|
||
|
rdfe_can_undo(Undo) :- % start undo
|
||
|
last_transaction(TID),
|
||
|
find_previous_undo(TID, Undo).
|
||
|
|
||
|
%% rdfe_transaction_name(+TID, -Name)
|
||
|
%
|
||
|
% Return name if the transaction is named.
|
||
|
|
||
|
rdfe_transaction_name(TID, Name) :-
|
||
|
transaction_name(TID, Name),
|
||
|
Name \== [].
|
||
|
|
||
|
%% rdfe_set_transaction_name(+Name)
|
||
|
%
|
||
|
% Set name of the current transaction
|
||
|
|
||
|
rdfe_set_transaction_name(Name) :-
|
||
|
current_transaction(TID), !,
|
||
|
assert(transaction_name(TID, Name)).
|
||
|
|
||
|
%% rdfe_transaction_member(+TID, -Action)
|
||
|
%
|
||
|
% Query actions inside a transaction to allow for quick update
|
||
|
% of visualisers.
|
||
|
|
||
|
rdfe_transaction_member(TID, Member) :-
|
||
|
( integer(TID)
|
||
|
-> Id = [TID|_]
|
||
|
; append(TID, _, Id)
|
||
|
),
|
||
|
undo_log(Id, Action, Subject, Predicate, Object),
|
||
|
user_transaction_member(Action, Subject, Predicate, Object, Member).
|
||
|
|
||
|
user_transaction_member(assert(_), Subject, Predicate, Object,
|
||
|
assert(Subject, Predicate, Object)) :- !.
|
||
|
user_transaction_member(retract(_), Subject, Predicate, Object,
|
||
|
retract(Subject, Predicate, Object)) :- !.
|
||
|
user_transaction_member(load_file(Path), -, -, -,
|
||
|
file(load(Path))) :- !.
|
||
|
user_transaction_member(unload_file(Path), -, -, -,
|
||
|
file(unload(Path))) :- !.
|
||
|
user_transaction_member(Update, Subject, Predicate, Object,
|
||
|
update(Subject, Predicate, Object, Update)).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* PROTECTION *
|
||
|
*******************************/
|
||
|
|
||
|
:- dynamic
|
||
|
rdf_source_permission/2, % file, ro/rw
|
||
|
rdf_current_default_file/2. % file, all/fallback
|
||
|
|
||
|
%% rdfe_set_file_property(+File, +Options)
|
||
|
%
|
||
|
% Set properties on the file. Options is one of
|
||
|
%
|
||
|
% * access(ro/rw)
|
||
|
% * default(all/fallback)
|
||
|
|
||
|
rdfe_set_file_property(File, access(Access)) :- !,
|
||
|
to_url(File, URL),
|
||
|
retractall(rdf_source_permission(URL, _)),
|
||
|
assert(rdf_source_permission(URL, Access)),
|
||
|
broadcast(rdf_file_property(URL, access(Access))).
|
||
|
rdfe_set_file_property(File, default(Type)) :-
|
||
|
to_url(File, URL),
|
||
|
rdfe_set_file_property(URL, access(rw)), % must be writeable
|
||
|
retractall(rdf_current_default_file(_,_)),
|
||
|
assert(rdf_current_default_file(URL, Type)),
|
||
|
broadcast(rdf_file_property(URL, default(Type))).
|
||
|
|
||
|
|
||
|
%% rdfe_get_file_property(+FileOrURL, ?Option).
|
||
|
%% rdfe_get_file_property(-URL, ?Option).
|
||
|
%
|
||
|
% Fetch file properties set with rdfe_set_file_property/2.
|
||
|
|
||
|
rdfe_get_file_property(FileOrURL, access(Access)) :-
|
||
|
( ground(FileOrURL)
|
||
|
-> to_url(FileOrURL, URL)
|
||
|
; rdf_source(_DB, URL),
|
||
|
FileOrURL = URL
|
||
|
),
|
||
|
( rdf_source_permission(URL, Access0)
|
||
|
-> Access0 = Access
|
||
|
; access_file(URL, write)
|
||
|
-> assert(rdf_source_permission(URL, rw)),
|
||
|
Access = rw
|
||
|
; assert(rdf_source_permission(URL, ro)),
|
||
|
Access = ro
|
||
|
).
|
||
|
rdfe_get_file_property(FileOrURL, default(Default)) :-
|
||
|
ground(FileOrURL),
|
||
|
to_url(FileOrURL, URL),
|
||
|
( rdf_current_default_file(URL, Default)
|
||
|
-> true
|
||
|
; FileOrURL = user,
|
||
|
Default = fallback
|
||
|
).
|
||
|
rdfe_get_file_property(URL, default(Default)) :-
|
||
|
( rdf_current_default_file(URL, Default)
|
||
|
-> true
|
||
|
; URL = user,
|
||
|
Default = fallback
|
||
|
).
|
||
|
|
||
|
|
||
|
%% check_file_protection(-Error)
|
||
|
%
|
||
|
% Check modification of all protected files
|
||
|
|
||
|
check_file_protection(Error) :-
|
||
|
( rdfe_get_file_property(File, access(ro)),
|
||
|
rdfe_is_modified(File)
|
||
|
-> Error = error(permission_error(modify, source, File), triple20)
|
||
|
; true
|
||
|
).
|
||
|
|
||
|
|
||
|
%% to_url(+Spec, -URL) is det.
|
||
|
%
|
||
|
% Convert a specification into a URL.
|
||
|
|
||
|
to_url(URL, URL) :-
|
||
|
atom(URL),
|
||
|
sub_atom(URL, B, _, _, '://'),
|
||
|
sub_atom(URL, 0, B, _, Protocol),
|
||
|
url_protocol(Protocol), !.
|
||
|
to_url(File, URL) :-
|
||
|
file_name_to_url(File, URL).
|
||
|
|
||
|
|
||
|
url_protocol(file).
|
||
|
url_protocol(http).
|
||
|
url_protocol(https).
|
||
|
url_protocol(ftp).
|
||
|
url_protocol(ftps).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* MODIFIED *
|
||
|
*******************************/
|
||
|
|
||
|
%% rdfe_is_modified(?Source)
|
||
|
%
|
||
|
% True if facts have been added, deleted or updated that have
|
||
|
% Source as `payload'.
|
||
|
|
||
|
rdfe_is_modified(Source) :-
|
||
|
rdf_source(DB, Source),
|
||
|
rdf_md5(DB, MD5),
|
||
|
( unmodified_md5(Source, UnmodifiedMD5)
|
||
|
-> true
|
||
|
; rdf_db:rdf_source(DB, Source, _Time, _Triples, UnmodifiedMD5)
|
||
|
),
|
||
|
UnmodifiedMD5 \== MD5.
|
||
|
|
||
|
|
||
|
rdfe_clear_modified :-
|
||
|
forall(rdf_graph(File),
|
||
|
rdfe_clear_modified(File)).
|
||
|
|
||
|
%% rdfe_clear_modified(+DB) is det.
|
||
|
%
|
||
|
% Consider the current state of DB as _unmodified_.
|
||
|
|
||
|
rdfe_clear_modified(DB) :-
|
||
|
atom(DB),
|
||
|
retractall(unmodified_md5(DB, _)),
|
||
|
rdf_md5(DB, MD5),
|
||
|
( rdf_db:rdf_source(DB, _File, _Time, _Triples, UnmodifiedMD5),
|
||
|
MD5 == UnmodifiedMD5
|
||
|
-> true
|
||
|
; assert(unmodified_md5(DB, MD5))
|
||
|
).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* WATERMARKS *
|
||
|
*******************************/
|
||
|
|
||
|
%% rdfe_set_watermark(Name)
|
||
|
%
|
||
|
% Create a watermark for undo and replay journal upto this point.
|
||
|
% The rest of the logic needs to be written later.
|
||
|
|
||
|
rdfe_set_watermark(Name) :-
|
||
|
rdfe_current_transaction(TID),
|
||
|
assert_action(TID, watermark(Name), -, -, -),
|
||
|
journal(watermark(TID, Name)).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* RESET *
|
||
|
*******************************/
|
||
|
|
||
|
%% rdfe_reset
|
||
|
%
|
||
|
% Clear database, undo, namespaces and journalling info.
|
||
|
|
||
|
rdfe_reset :-
|
||
|
rdfe_reset_journal,
|
||
|
rdfe_reset_ns,
|
||
|
rdfe_reset_undo,
|
||
|
rdf_reset_db,
|
||
|
broadcast(rdf_reset).
|
||
|
|
||
|
%% rdfe_reset_journal
|
||
|
%
|
||
|
% If a journal is open, close it using rdfe_close_journal/0
|
||
|
|
||
|
rdfe_reset_journal :-
|
||
|
( rdfe_current_journal(_)
|
||
|
-> rdfe_close_journal
|
||
|
; true
|
||
|
).
|
||
|
|
||
|
rdfe_reset_undo :-
|
||
|
retractall(undo_log(_,_,_,_,_)),
|
||
|
retractall(current_transaction(_)),
|
||
|
retractall(transaction_name(_,_)),
|
||
|
retractall(undo_marker(_,_)),
|
||
|
retractall(unmodified_md5(_, _)),
|
||
|
retractall(snapshot_file(_)).
|
||
|
|
||
|
% close possible open journal at exit. Using a Prolog hook
|
||
|
% guarantees closure, even for most crashes.
|
||
|
|
||
|
:- at_halt(rdfe_reset_journal).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* JOURNALLING *
|
||
|
*******************************/
|
||
|
|
||
|
journal_version(1).
|
||
|
|
||
|
%% rdfe_open_journal(+File, +Mode) is det.
|
||
|
%
|
||
|
% Open a journal writing to File in Mode. Mode is one of
|
||
|
%
|
||
|
% * read
|
||
|
% Open and replay the journal
|
||
|
%
|
||
|
% * write
|
||
|
% Delete current journal and create a fresh one
|
||
|
%
|
||
|
% * append
|
||
|
% Read and replay the existing journal and append new
|
||
|
% modifications to the File.
|
||
|
|
||
|
rdfe_open_journal(_, _) :- % already open
|
||
|
journal(_, _, _), !.
|
||
|
rdfe_open_journal(File, read) :- !,
|
||
|
absolute_file_name(File,
|
||
|
[ extensions([rdfj, '']),
|
||
|
access(read)
|
||
|
],
|
||
|
Path),
|
||
|
rdfe_replay_journal(Path),
|
||
|
rdfe_clear_modified.
|
||
|
rdfe_open_journal(File, write) :- !,
|
||
|
absolute_file_name(File,
|
||
|
[ extensions([rdfj, '']),
|
||
|
access(write)
|
||
|
],
|
||
|
Path),
|
||
|
open(Path, write, Stream, [close_on_abort(false)]),
|
||
|
assert(journal(Path, write, Stream)),
|
||
|
get_time(T),
|
||
|
journal_open(start, T).
|
||
|
rdfe_open_journal(File, append) :-
|
||
|
working_directory(CWD, CWD),
|
||
|
absolute_file_name(File,
|
||
|
[ extensions([rdfj, '']),
|
||
|
relative_to(CWD),
|
||
|
access(write)
|
||
|
],
|
||
|
Path),
|
||
|
( exists_file(Path)
|
||
|
-> rdfe_replay_journal(Path),
|
||
|
rdfe_clear_modified,
|
||
|
get_time(T),
|
||
|
assert(journal(Path, append(T), []))
|
||
|
; rdfe_open_journal(Path, write)
|
||
|
).
|
||
|
|
||
|
|
||
|
journal_open(Type, Time) :-
|
||
|
journal_comment(Type, Time),
|
||
|
SecTime is round(Time),
|
||
|
journal_version(Version),
|
||
|
Start =.. [ Type, [ time(SecTime),
|
||
|
version(Version)
|
||
|
]
|
||
|
],
|
||
|
journal(Start),
|
||
|
broadcast(rdf_journal(Start)).
|
||
|
|
||
|
journal_comment(start, Time) :-
|
||
|
journal(_, _, Stream),
|
||
|
format_time(string(String), '%+', Time),
|
||
|
format(Stream,
|
||
|
'/* Triple20 Journal File\n\n \
|
||
|
Created: ~w\n \
|
||
|
Triple20 by Jan Wielemaker <wielemak@science.uva.nl>\n\n \
|
||
|
EDIT WITH CARE!\n\
|
||
|
*/~n~n', [String]).
|
||
|
journal_comment(resume, Time) :-
|
||
|
journal(_, _, Stream),
|
||
|
format_time(string(String), '%+', Time),
|
||
|
format(Stream,
|
||
|
'\n\
|
||
|
/* Resumed: ~w\n\
|
||
|
*/~n~n', [String]).
|
||
|
|
||
|
%% rdfe_close_journal
|
||
|
%
|
||
|
% Close the journal. Automatically called from at program
|
||
|
% termination from at_halt/1.
|
||
|
|
||
|
rdfe_close_journal :-
|
||
|
get_time(T),
|
||
|
SecTime is round(T),
|
||
|
journal(end([ time(SecTime)
|
||
|
])),
|
||
|
retract(journal(_, Mode, Stream)),
|
||
|
( Mode = append(_)
|
||
|
-> true
|
||
|
; close(Stream)
|
||
|
).
|
||
|
|
||
|
%% rdfe_current_journal(-Path)
|
||
|
%
|
||
|
% Query the currently open journal
|
||
|
|
||
|
rdfe_current_journal(Path) :-
|
||
|
journal(Path, _Mode, _Stream).
|
||
|
|
||
|
journal(Term) :-
|
||
|
journal(Path, append(T), _), !,
|
||
|
( Term = end(_)
|
||
|
-> true
|
||
|
; open(Path, append, Stream, [close_on_abort(false)]),
|
||
|
retractall(journal(Path, _, _)),
|
||
|
assert(journal(Path, append, Stream)),
|
||
|
journal_open(resume, T),
|
||
|
journal(Term)
|
||
|
).
|
||
|
journal(Term) :-
|
||
|
( journal(_, _, Stream)
|
||
|
-> write_journal(Term, Stream),
|
||
|
flush_output(Stream)
|
||
|
; broadcast(rdf_no_journal(Term))
|
||
|
).
|
||
|
|
||
|
write_journal(commit(TID, Time), Stream) :- !,
|
||
|
format(Stream, 'commit(~q, ~2f).~n~n', [TID, Time]).
|
||
|
write_journal(Term, Stream) :-
|
||
|
format(Stream, '~q.~n', [Term]).
|
||
|
|
||
|
|
||
|
%% rdfe_replay_journal(+File)
|
||
|
%
|
||
|
% Replay a journal file. For now this is our cheap way to deal
|
||
|
% with save/load. Future versions may be more clever when dealing
|
||
|
% with the version information stored in the journal.
|
||
|
|
||
|
rdfe_replay_journal(File) :-
|
||
|
absolute_file_name(File,
|
||
|
[ extensions([rdfj, '']),
|
||
|
access(read)
|
||
|
],
|
||
|
Path),
|
||
|
open(Path, read, Stream),
|
||
|
replay(Stream),
|
||
|
close(Stream).
|
||
|
|
||
|
replay(Stream) :-
|
||
|
read(Stream, Term),
|
||
|
replay(Term, Stream).
|
||
|
|
||
|
replay(end_of_file, _) :- !.
|
||
|
replay(start(_Attributes), Stream) :- !,
|
||
|
read(Stream, Term),
|
||
|
replay(Term, Stream).
|
||
|
replay(resume(_Attributes), Stream) :- !,
|
||
|
read(Stream, Term),
|
||
|
replay(Term, Stream).
|
||
|
replay(end(_Attributes), Stream) :- !,
|
||
|
read(Stream, Term),
|
||
|
replay(Term, Stream).
|
||
|
replay(Term0, Stream) :-
|
||
|
replay_transaction(Term0, Stream),
|
||
|
read(Stream, Term),
|
||
|
replay(Term, Stream).
|
||
|
|
||
|
replay_transaction(Term0, Stream) :-
|
||
|
collect_transaction(Term0, Stream, Transaction, Last),
|
||
|
( committed_transaction(Last)
|
||
|
-> replay_actions(Transaction)
|
||
|
; true
|
||
|
).
|
||
|
|
||
|
collect_transaction(End, _, [], End) :-
|
||
|
ends_transaction(End), !.
|
||
|
collect_transaction(A, Stream, [A|T], End) :-
|
||
|
read(Stream, Term),
|
||
|
collect_transaction(Term, Stream, T, End).
|
||
|
|
||
|
committed_transaction(commit(_)).
|
||
|
committed_transaction(commit(_, _)).
|
||
|
|
||
|
ends_transaction(end_of_file).
|
||
|
ends_transaction(commit(_)).
|
||
|
ends_transaction(commit(_, _)).
|
||
|
ends_transaction(rollback(_)).
|
||
|
ends_transaction(end(_)).
|
||
|
ends_transaction(start(_)).
|
||
|
|
||
|
replay_actions([]).
|
||
|
replay_actions([H|T]) :-
|
||
|
( replay_action(H)
|
||
|
-> replay_actions(T)
|
||
|
; print_message(warning,
|
||
|
rdf_replay_failed(H)),
|
||
|
( debugging(journal)
|
||
|
-> gtrace,
|
||
|
replay_actions([H|T])
|
||
|
; replay_actions(T)
|
||
|
)
|
||
|
).
|
||
|
|
||
|
|
||
|
%% replay_action(+Action)
|
||
|
%
|
||
|
% Replay actions from the journal. Tricky is rdf_load/3. It should
|
||
|
% reload the file in the state it was in at the moment it was
|
||
|
% created. For now this has been hacked for files that were empry
|
||
|
% at the moment they where loaded (e.g. created from `new_file' in
|
||
|
% our GUI prototype). How to solve this? We could warn if the file
|
||
|
% appears changed, but this isn't really easy as copying and OS
|
||
|
% differences makes it hard to decide on changes by length as well
|
||
|
% as modification time. Alternatively we could save the state in
|
||
|
% seperate quick-load states.
|
||
|
|
||
|
replay_action(retract(_, Subject, Predicate, Object, PayLoad)) :-
|
||
|
rdf_retractall(Subject, Predicate, Object, PayLoad).
|
||
|
replay_action(assert(_, Subject, Predicate, Object, PayLoad)) :-
|
||
|
rdf_assert(Subject, Predicate, Object, PayLoad).
|
||
|
replay_action(update(_, Subject, Predicate, Object, Action)) :-
|
||
|
rdf_update(Subject, Predicate, Object, Action).
|
||
|
replay_action(update(_, Subject, Predicate, Object, Payload, Action)) :-
|
||
|
rdf_update(Subject, Predicate, Object, Payload, Action).
|
||
|
replay_action(rdf_load(_, File, Options)) :-
|
||
|
memberchk(md5(MD5), Options),
|
||
|
snapshot_file(File, MD5,
|
||
|
[ access(read),
|
||
|
file_errors(fail)
|
||
|
],
|
||
|
Path), !,
|
||
|
debug(snapshot, 'Reloading snapshot ~w~n', [Path]),
|
||
|
load_snapshot(File, Path).
|
||
|
replay_action(rdf_load(_, File, Options)) :-
|
||
|
find_file(File, Options, Path),
|
||
|
( memberchk(triples(0), Options),
|
||
|
memberchk(modified(Modified), Options)
|
||
|
-> rdf_retractall(_,_,_,Path:_),
|
||
|
retractall(rdf_db:rdf_source(Path, _, _, _)), % TBD: move
|
||
|
rdf_md5(Path, MD5),
|
||
|
assert(rdf_db:rdf_source(Path, Modified, 0, MD5))
|
||
|
; rdf_load(Path)
|
||
|
).
|
||
|
replay_action(rdf_unload(_, Source)) :-
|
||
|
rdf_unload(Source).
|
||
|
replay_action(ns(_, register(ID, URI))) :- !,
|
||
|
rdf_register_ns(ID, URI).
|
||
|
replay_action(ns(_, unregister(ID, URI))) :-
|
||
|
retractall(rdf_db:ns(ID, URI)).
|
||
|
replay_action(watermark(_, _Name)) :-
|
||
|
true.
|
||
|
|
||
|
find_file(File, _, File) :-
|
||
|
exists_file(File), !.
|
||
|
find_file(File, Options, Path) :-
|
||
|
memberchk(pwd(PWD), Options),
|
||
|
make_path(File, PWD, Path),
|
||
|
exists_file(Path), !.
|
||
|
|
||
|
%% make_path(+File, +PWD, -Path)
|
||
|
%
|
||
|
% Return location of File relative to PWD, Parent of PWD, etc. (TBD)
|
||
|
|
||
|
make_path(File, PWD, Path) :-
|
||
|
atom_concat(PWD, /, PWD2),
|
||
|
atom_concat(PWD2, Path, File).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* MESSAGES *
|
||
|
*******************************/
|
||
|
|
||
|
:- multifile
|
||
|
prolog:message/3,
|
||
|
user:message_hook/3.
|
||
|
|
||
|
% Catch messages.
|
||
|
|
||
|
prolog:message(rdf_replay_failed(Term)) -->
|
||
|
[ 'RDFDB: Replay of ~p failed'-[Term] ].
|
||
|
prolog:message(rdf_undo_failed(Term)) -->
|
||
|
[ 'RDFDB: Undo of ~p failed'-[Term] ].
|