View source with raw comments or as raw
   1/*  Part of SWI-Prolog
   2
   3    Author:        Jan Wielemaker
   4    E-mail:        J.Wielemaker@vu.nl
   5    WWW:           http://www.swi-prolog.org
   6    Copyright (c)  2003-2013, University of Amsterdam
   7                              VU University Amsterdam
   8    All rights reserved.
   9
  10    Redistribution and use in source and binary forms, with or without
  11    modification, are permitted provided that the following conditions
  12    are met:
  13
  14    1. Redistributions of source code must retain the above copyright
  15       notice, this list of conditions and the following disclaimer.
  16
  17    2. Redistributions in binary form must reproduce the above copyright
  18       notice, this list of conditions and the following disclaimer in
  19       the documentation and/or other materials provided with the
  20       distribution.
  21
  22    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  23    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  24    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  25    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  26    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  27    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  28    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  29    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  30    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  31    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  32    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  33    POSSIBILITY OF SUCH DAMAGE.
  34*/
  35
  36:- module(rdf_edit,
  37          [ rdfe_assert/3,              % Sub, Pred, Obj
  38            rdfe_assert/4,              % Sub, Pred, Obj, PayLoad
  39            rdfe_retractall/3,          % Sub, Pred, Obj
  40            rdfe_retractall/4,          % Sub, Pred, Obj, PayLoad
  41            rdfe_update/4,              % Sub, Pred, Obj, +Action
  42            rdfe_update/5,              % Sub, Pred, Obj, +PayLoad, +Action
  43            rdfe_load/1,                % +File
  44            rdfe_load/2,                % +File, +Options
  45            rdfe_delete/1,              % +Resource
  46
  47            rdfe_register_ns/2,         % +Id, +URI
  48            rdfe_unregister_ns/2,       % +Id, +URI
  49
  50            rdfe_reset/0,               % clear everything
  51
  52            rdfe_transaction/1,         % :Goal
  53            rdfe_transaction/2,         % :Goal, +Name
  54            rdfe_transaction_member/2,  % +Transactions, -Action
  55            rdfe_transaction_name/2,    % +Transactions, -Name
  56            rdfe_set_transaction_name/1,% +Name
  57
  58            rdfe_set_watermark/1,       % +Name
  59
  60            rdfe_undo/0,                %
  61            rdfe_redo/0,
  62            rdfe_can_undo/1,            % -TID
  63            rdfe_can_redo/1,            % -TID
  64
  65            rdfe_set_file_property/2,   % +File, +Property
  66            rdfe_get_file_property/2,   % ?File, ?Property
  67
  68            rdfe_is_modified/1,         % ?File
  69            rdfe_clear_modified/1,      % +File
  70
  71            rdfe_open_journal/2,        % +File, +Mode
  72            rdfe_close_journal/0,
  73            rdfe_replay_journal/1,      % +File
  74            rdfe_current_journal/1,     % -Path
  75
  76            rdfe_snapshot_file/1        % -File
  77          ]).
  78:- use_module(rdf_db).
  79:- use_module(library(broadcast)).
  80:- use_module(library(lists)).
  81:- use_module(library(debug)).
  82:- use_module(library(uri)).
  83
  84:- meta_predicate
  85    rdfe_transaction(0),
  86    rdfe_transaction(0, +).
  87
  88:- predicate_options(rdfe_load/2, 2,
  89                     [pass_to(rdf_db:rdf_load/2, 2)]).
  90
  91:- dynamic
  92    undo_log/5,                     % TID, Action, Subj, Pred, Obj
  93    current_transaction/1,          % TID
  94    transaction_name/2,             % TID, Name
  95    undo_marker/2,                  % Mode, TID
  96    journal/3,                      % Path, Mode, Stream
  97    snapshot_file/1.                % File
  98
  99/** <module> RDF edit layer
 100This library provides a number of functions on top of the rdf_db module:
 101
 102    * Broadcast modifications
 103    * Provide undo/redo
 104
 105@tbd    This library must be rewritten using rdf_monitor/3.  This allows
 106        using edit layer without having to choose between rdf_ and rdfe_
 107        predicates.
 108
 109@see    rdf_persistency.pl provides reliable persistency, but without
 110        changes boardcasting and undo/redo.
 111*/
 112
 113:- rdf_meta
 114    rdfe_assert(r,r,o),
 115    rdfe_assert(r,r,o,+),
 116    rdfe_retractall(r,r,o),
 117    rdfe_update(r,r,o,t),
 118    rdfe_delete(r),
 119    rdfe_transaction(:),
 120    rdfe_transaction(:, +).
 121
 122
 123                 /*******************************
 124                 *     BASIC EDIT OPERATIONS    *
 125                 *******************************/
 126
 127rdfe_assert(Subject, Predicate, Object) :-
 128    rdfe_assert(Subject, Predicate, Object, user).
 129
 130rdfe_assert(Subject, Predicate, Object, PayLoad) :-
 131    rdf_assert(Subject, Predicate, Object, PayLoad),
 132    rdfe_current_transaction(TID),
 133    assert_action(TID, assert(PayLoad), Subject, Predicate, Object),
 134    journal(assert(TID, Subject, Predicate, Object, PayLoad)).
 135
 136rdfe_retractall(Subject, Predicate, Object) :-
 137    rdfe_retractall(Subject, Predicate, Object, _).
 138
 139rdfe_retractall(Subject, Predicate, Object, PayLoad) :-
 140    rdfe_current_transaction(TID),
 141    (   rdf(Subject, Predicate, Object, PayLoad),
 142        assert_action(TID, retract(PayLoad), Subject, Predicate, Object),
 143        journal(retract(TID, Subject, Predicate, Object, PayLoad)),
 144        fail
 145    ;   true
 146    ),
 147    rdf_retractall(Subject, Predicate, Object, PayLoad).
 148
 149%!  rdfe_update(+Subject, +Predicate, +Object, +Action)
 150%
 151%   Update an existing triple.  Possible actions are:
 152%
 153%!          subject(+Subject)
 154%!          predicate(+Predicate)
 155%!          object(+Object)
 156%!          source(+Source)
 157
 158rdfe_update(Subject, Predicate, Object, Action) :-
 159    rdfe_current_transaction(TID),
 160    rdf_update(Subject, Predicate, Object, Action),
 161    (   Action = object(New)
 162    ->  assert_action(TID, object(Object), Subject, Predicate, New)
 163    ;   Action = predicate(New)
 164    ->  assert_action(TID, predicate(Predicate), Subject, New, Object)
 165    ;   Action = subject(New)
 166    ->  assert_action(TID, subject(Subject), New, Predicate, Object)
 167    ;   Action = source(New)
 168    ->  forall(rdf(Subject, Predicate, Object, PayLoad),
 169               assert_action(TID, source(PayLoad, New),
 170                             Subject, Predicate, Object))
 171    ),
 172    journal(update(TID, Subject, Predicate, Object, Action)).
 173
 174rdfe_update(Subject, Predicate, Object, PayLoad, Action) :-
 175    rdfe_current_transaction(TID),
 176    rdf_update(Subject, Predicate, Object, PayLoad, Action),
 177    (   Action = source(New)
 178    ->  assert_action(TID, source(PayLoad, New),
 179                      Subject, Predicate, Object)
 180    ;   throw(tbd)                  % source is used internally
 181    ),
 182    journal(update(TID, Subject, Predicate, Object, PayLoad, Action)).
 183
 184%!  rdfe_delete(+Subject)
 185%
 186%   Delete a subject and all we know about it. This is a bit tricky.
 187%   If we are involved in transitive relations, should we re-joint
 188%   these in this module?
 189
 190rdfe_delete(Subject) :-
 191    rdfe_transaction(delete(Subject)).
 192
 193delete(Subject) :-
 194    rdfe_retractall(Subject, _, _),
 195    rdfe_retractall(_, Subject, _),
 196    rdfe_retractall(_, _, Subject).
 197
 198
 199                 /*******************************
 200                 *         FILE HANDLING        *
 201                 *******************************/
 202
 203%!  rdfe_load(+File) is det.
 204%!  rdfe_load(+File, +Options) is det.
 205%
 206%   Load an RDF file and record this action including version information
 207%   to facilitate reliable reload.
 208
 209rdfe_load(File) :-
 210    rdfe_load(File, []).
 211
 212
 213rdfe_load(File, Options) :-
 214    rdfe_current_transaction(TID),
 215    absolute_file_name(File,
 216                       [ access(read),
 217                         extensions([rdf,rdfs,owl,''])
 218                       ], Path),
 219    rdf_load(Path,
 220             [ graph(Graph),
 221               modified(Modified)
 222             | Options
 223             ]),
 224    (   Modified == not_modified
 225    ->  true
 226    ;   absolute_file_name('.', PWD),
 227        size_file(Path, Size),
 228        (   Modified = last_modified(Stamp)
 229        ->  true
 230        ;   time_file(Path, Stamp)
 231        ),
 232        SecTime is round(Stamp),
 233        rdf_statistics(triples_by_graph(Graph, Triples)),
 234        rdf_md5(Graph, MD5),
 235        assert_action(TID, load_file(Path), -, -, -),
 236        journal(rdf_load(TID,
 237                         Path,
 238                         [ pwd(PWD),
 239                           size(Size),
 240                           modified(SecTime),
 241                           triples(Triples),
 242                           md5(MD5),
 243                           from(File)
 244                         ])),
 245        ensure_snapshot(Path)
 246    ).
 247
 248
 249rdfe_unload(Path) :-
 250    rdfe_current_transaction(TID),
 251    rdf_unload(Path),
 252    assert_action(TID, unload_file(Path), -, -, -),
 253    journal(rdf_unload(TID, Path)).
 254
 255
 256%!  ensure_snapshot(+Path)
 257%
 258%   Ensure we have a snapshot of Path if we are making a journal, so
 259%   we can always reload the snapshot to ensure exactly the same
 260%   state.
 261
 262ensure_snapshot(Path) :-
 263    rdfe_current_journal(_),
 264    rdf_md5(Path, MD5),
 265    (   snapshot_file(Path, MD5,
 266                      [ access(read),
 267                        file_errors(fail)
 268                      ],
 269                      File)
 270    ->  debug(snapshot, 'Existing snapshot for ~w on ~w', [Path, File])
 271    ;   snapshot_file(Path, MD5,
 272                      [ access(write)
 273                      ],
 274                      File),
 275        debug(snapshot, 'Saving snapshot for ~w to ~w', [Path, File]),
 276        rdf_save_db(File, Path)
 277    ),
 278    assert(snapshot_file(File)).
 279ensure_snapshot(_).
 280
 281
 282%!  load_snapshot(+Source, +Path)
 283%
 284%   Load triples from the given snapshot   file. One of the troubles
 285%   is the time-stamp to avoid rdf_make/0   from reloading the file.
 286%   for the time being we use 1e12, which   is  a lot further in the
 287%   future than this system is going to live.
 288
 289load_snapshot(Source, Path) :-
 290    statistics(cputime, T0),
 291    rdf_load_db(Path),
 292    statistics(cputime, T1),
 293    Time is T1 - T0,
 294    rdf_statistics(triples_by_graph(Source, Triples)),
 295    rdf_md5(Source, MD5),
 296                                    % 1e10: modified far in the future
 297    assert(rdf_db:rdf_source(Source, 1e12, Triples, MD5)),
 298    print_message(informational,
 299                  rdf(loaded(Source, Triples, snapshot(Time)))),
 300    assert(snapshot_file(Path)).
 301
 302
 303%!  snapshot_file(+Path, +MD5, +Access, -File)
 304%
 305%   Find existing snapsnot file or location to save a new one.
 306
 307snapshot_file(Path, MD5, Options, SnapShot) :-
 308    file_base_name(Path, Base),
 309    atomic_list_concat([Base, @, MD5], File),
 310    absolute_file_name(snapshot(File),
 311                       [ extensions([trp])
 312                       | Options
 313                       ],
 314                       SnapShot).
 315
 316
 317%!  rdfe_snapshot_file(-File)
 318%
 319%   Enumerate the MD5 snapshot files required to restore the current
 320%   journal file. Using this  call  we   can  write  a  routine that
 321%   packages the journal file with all required snapshots to restore
 322%   the journal on another computer.
 323
 324rdfe_snapshot_file(File) :-
 325    snapshot_file(File).
 326
 327
 328                 /*******************************
 329                 *      NAMESPACE HANDLING      *
 330                 *******************************/
 331
 332:- dynamic
 333    system_ns/2.
 334:- volatile
 335    system_ns/2.
 336
 337%!  rdfe_register_ns(Id, URI)
 338%
 339%   Encapsulation of rdf_register_ns(Id, URI)
 340
 341rdfe_register_ns(Id, URI) :-
 342    rdf_db:ns(Id, URI),
 343    !.
 344rdfe_register_ns(Id, URI) :-
 345    save_system_ns,
 346    rdfe_current_transaction(TID),
 347    rdf_register_ns(Id, URI),
 348    broadcast(rdf_ns(register(Id, URI))),
 349    assert_action(TID, ns(register(Id, URI)), -, -, -),
 350    journal(ns(TID, register(Id, URI))).
 351
 352rdfe_unregister_ns(Id, URI) :-
 353    save_system_ns,
 354    rdfe_current_transaction(TID),
 355    retractall(rdf_db:ns(Id, URI)),
 356    broadcast(rdf_ns(unregister(Id, URI))),
 357    assert_action(TID, ns(unregister(Id, URI)), -, -, -),
 358    journal(ns(TID, unregister(Id, URI))).
 359
 360%       rdfe_register_ns/0
 361%
 362%       Reset namespaces to the state they where before usage of the
 363%       rdf_edit layer.
 364
 365rdfe_reset_ns :-
 366    (   system_ns(_, _)
 367    ->  retractall(rdf_db:ns(Id, URI)),
 368        forall(system_ns(Id, URI), assert(rdb_db:ns(Id, URI)))
 369    ;   true
 370    ).
 371
 372save_system_ns :-
 373    system_ns(_, _),
 374    !.             % already done
 375save_system_ns :-
 376    forall(rdf_db:ns(Id, URI), assert(system_ns(Id, URI))).
 377
 378
 379                 /*******************************
 380                 *         TRANSACTIONS         *
 381                 *******************************/
 382
 383%!  rdfe_transaction(:Goal)
 384%
 385%   Run Goal, recording all modifications   as a single transaction.
 386%   If  Goal  raises  an  exception  or    fails,  all  changes  are
 387%   rolled-back.
 388
 389rdfe_transaction(Goal) :-
 390    rdfe_transaction(Goal, []).
 391rdfe_transaction(Goal, Name) :-
 392    rdfe_begin_transaction(Name),
 393    (   catch(Goal, E, true)
 394    ->  (   var(E)
 395        ->  check_file_protection(Error),
 396            (   var(Error)
 397            ->  rdfe_commit
 398            ;   rdfe_rollback,
 399                throw(Error)
 400            )
 401        ;   rdfe_rollback,
 402            throw(E)
 403        )
 404    ;   rdfe_rollback,
 405        fail
 406    ).
 407
 408%!  rdfe_begin_transaction
 409%
 410%   Start a transaction.  This is followed by either rdfe_end_transaction
 411%   or rdfe_rollback.  Transactions may be nested.
 412
 413rdfe_begin_transaction(Name) :-
 414    current_transaction(TID),      % nested transaction
 415    !,
 416    append(TID, [1], TID2),
 417    asserta(current_transaction(TID2)),
 418    assert(transaction_name(TID2, Name)).
 419rdfe_begin_transaction(Name) :-         % toplevel transaction
 420    flag(rdf_edit_tid, TID, TID+1),
 421    asserta(current_transaction([TID])),
 422    assert(transaction_name(TID, Name)).
 423
 424rdfe_current_transaction(TID) :-
 425    current_transaction(TID),
 426    !.
 427rdfe_current_transaction(_) :-
 428    throw(error(existence_error(rdf_transaction, _), _)).
 429
 430rdfe_commit :-
 431    retract(current_transaction(TID)),
 432    !,
 433    retractall(undo_marker(_, _)),
 434    (   rdfe_transaction_member(TID, _)
 435    ->  get_time(Time),             % transaction is not empty
 436        journal(commit(TID, Time)),
 437        (   TID = [Id]
 438        ->  broadcast(rdf_transaction(Id))
 439        ;   true
 440        )
 441    ;   true
 442    ).
 443
 444rdfe_rollback :-
 445    retract(current_transaction(TID)),
 446    !,
 447    journal(rollback(TID)),
 448    rollback(TID).
 449
 450%!  rollback(+TID)
 451%
 452%   This is the same as undo/1, but it must not record the undone
 453%   actions as rollbacks cannot be `redone'.  Somehow there should
 454%   be a cleaner way to distinguish between transactional operations
 455%   and plain operations.
 456
 457rollback(TID) :-
 458    append(TID, _, Id),
 459    (   retract(undo_log(Id, Action, Subject, Predicate, Object)),
 460        (   rollback(Action, Subject, Predicate, Object)
 461        ->  fail
 462        ;   print_message(error,
 463                          rdf_undo_failed(undo(Action, Subject,
 464                                               Predicate, Object))),
 465            fail
 466        )
 467    ;   true
 468    ).
 469
 470rollback(assert(PayLoad), Subject, Predicate, Object) :-
 471    !,
 472    rdf_retractall(Subject, Predicate, Object, PayLoad).
 473rollback(retract(PayLoad), Subject, Predicate, Object) :-
 474    !,
 475    rdf_assert(Subject, Predicate, Object, PayLoad).
 476rollback(Action, Subject, Predicate, Object) :-
 477    action(Action),
 478    !,
 479    rdf_update(Subject, Predicate, Object, Action).
 480
 481
 482assert_action(TID, Action, Subject, Predicate, Object) :-
 483    asserta(undo_log(TID, Action, Subject, Predicate, Object)).
 484
 485%!  undo(+TID)
 486%
 487%   Undo a transaction as well as possible transactions nested into
 488%   it.
 489
 490undo(TID) :-
 491    append(TID, _, Id),
 492    (   retract(undo_log(Id, Action, Subject, Predicate, Object)),
 493        (   undo(Action, Subject, Predicate, Object)
 494        ->  fail
 495        ;   print_message(warning,
 496                          rdf_undo_failed(undo(Action, Subject,
 497                                               Predicate, Object))),
 498            fail
 499        )
 500    ;   true
 501    ).
 502
 503undo(assert(PayLoad), Subject, Predicate, Object) :-
 504    !,
 505    rdfe_retractall(Subject, Predicate, Object, PayLoad).
 506undo(retract(PayLoad), Subject, Predicate, Object) :-
 507    !,
 508    rdfe_assert(Subject, Predicate, Object, PayLoad).
 509undo(source(Old, New), Subject, Predicate, Object) :-
 510    !,
 511    rdfe_update(Subject, Predicate, Object, Old, source(New)).
 512undo(ns(Action), -, -, -) :-
 513    !,
 514    (   Action = register(Id, URI)
 515    ->  rdfe_unregister_ns(Id, URI)
 516    ;   Action = unregister(Id, URI)
 517    ->  rdfe_register_ns(Id, URI)
 518    ).
 519undo(load_file(Path), -, -, -) :-
 520    !,
 521    rdfe_unload(Path).
 522undo(unload_file(Path), -, -, -) :-
 523    !,
 524    rdfe_load(Path).
 525undo(Action, Subject, Predicate, Object) :-
 526    action(Action),
 527    !,
 528    rdfe_update(Subject, Predicate, Object, Action).
 529
 530action(subject(_)).
 531action(predicate(_)).
 532action(object(_)).
 533
 534%!  rdfe_undo
 535%
 536%   Undo a (toplevel) transaction. More calls do further undo. The
 537%   `Undone' actions are re-added to the undo log, so the user can
 538%   redo them.  Fails if there are no more undo/redo transactions.
 539
 540rdfe_undo :-
 541    undo_marker(undo, TID),
 542    !,
 543    (   undo_previous(TID, UnDone)
 544    ->  retractall(undo_marker(_, _)),
 545        assert(undo_marker(undo, UnDone)),
 546        broadcast(rdf_undo(undo, UnDone))
 547    ;   fail                        % start of undo log
 548    ).
 549rdfe_undo :-
 550    retract(undo_marker(redo, _)),
 551    !,
 552    last_transaction(TID),
 553    undo_previous(TID, UnDone),
 554    assert(undo_marker(undo, UnDone)),
 555    broadcast(rdf_undo(undo, UnDone)).
 556rdfe_undo :-
 557    last_transaction(TID),
 558    undo_previous(TID, UnDone),
 559    assert(undo_marker(undo, UnDone)),
 560    broadcast(rdf_undo(undo, UnDone)).
 561
 562find_previous_undo(-1, _) :-
 563    !,
 564    fail.
 565find_previous_undo(TID, TID) :-
 566    undo_log([TID|_], _, _, _, _),
 567    !.
 568find_previous_undo(TID0, TID) :-
 569    TID1 is TID0 - 1,
 570    find_previous_undo(TID1, TID).
 571
 572undo_previous(TID, Undone) :-
 573    find_previous_undo(TID, Undone),
 574    rdfe_transaction(undo([Undone])).
 575
 576last_transaction(TID) :-
 577    undo_log([TID|_], _, _, _, _),
 578    !.
 579
 580%!  rdfe_redo
 581%
 582%   Start a redo-session
 583
 584rdfe_redo :-
 585    (   retract(undo_marker(undo, _))
 586    ->  last_transaction(TID),
 587        undo_previous(TID, UnDone),
 588        assert(undo_marker(redo, UnDone)),
 589        broadcast(rdf_undo(redo, UnDone))
 590    ;   retract(undo_marker(redo, TID))
 591    ->  undo_previous(TID, UnDone),
 592        assert(undo_marker(redo, UnDone)),
 593        broadcast(rdf_undo(redo, UnDone))
 594    ;   true
 595    ).
 596
 597
 598%!  rdfe_can_redo(-TID) is semidet.
 599%!  rdfe_can_undo(-TID) is semidet.
 600%
 601%   Check if we can undo and if so return the id of the transaction
 602%   that will be un/re-done.  A subsequent call to rdfe_transaction_name
 603%   can be used to give a hint in the UI.
 604
 605rdfe_can_redo(Redo) :-
 606    undo_marker(undo, _),
 607    !,
 608    last_transaction(TID),
 609    find_previous_undo(TID, Redo).
 610rdfe_can_redo(Redo) :-
 611    undo_marker(redo, TID),
 612    find_previous_undo(TID, Redo).
 613
 614rdfe_can_undo(Undo) :-                  % continue undo
 615    undo_marker(undo, TID),
 616    !,
 617    find_previous_undo(TID, Undo).
 618rdfe_can_undo(Undo) :-                  % start undo
 619    last_transaction(TID),
 620    find_previous_undo(TID, Undo).
 621
 622%!  rdfe_transaction_name(+TID, -Name)
 623%
 624%   Return name if the transaction is named.
 625
 626rdfe_transaction_name(TID, Name) :-
 627    transaction_name(TID, Name),
 628    Name \== [].
 629
 630%!  rdfe_set_transaction_name(+Name)
 631%
 632%   Set name of the current transaction
 633
 634rdfe_set_transaction_name(Name) :-
 635    current_transaction(TID),
 636    !,
 637    assert(transaction_name(TID, Name)).
 638
 639%!  rdfe_transaction_member(+TID, -Action)
 640%
 641%   Query actions inside a transaction to allow for quick update
 642%   of visualisers.
 643
 644rdfe_transaction_member(TID, Member) :-
 645    (   integer(TID)
 646    ->  Id = [TID|_]
 647    ;   append(TID, _, Id)
 648    ),
 649    undo_log(Id, Action, Subject, Predicate, Object),
 650    user_transaction_member(Action, Subject, Predicate, Object, Member).
 651
 652user_transaction_member(assert(_), Subject, Predicate, Object,
 653                        assert(Subject, Predicate, Object)) :- !.
 654user_transaction_member(retract(_), Subject, Predicate, Object,
 655                        retract(Subject, Predicate, Object)) :- !.
 656user_transaction_member(load_file(Path), -, -, -,
 657                        file(load(Path))) :- !.
 658user_transaction_member(unload_file(Path), -, -, -,
 659                        file(unload(Path))) :- !.
 660user_transaction_member(Update, Subject, Predicate, Object,
 661                        update(Subject, Predicate, Object, Update)).
 662
 663
 664                 /*******************************
 665                 *           PROTECTION         *
 666                 *******************************/
 667
 668:- dynamic
 669    rdf_source_permission/2,        % file, ro/rw
 670    rdf_current_default_file/2.     % file, all/fallback
 671
 672%!  rdfe_set_file_property(+File, +Options)
 673%
 674%   Set properties on the file.  Options is one of
 675%
 676%           * access(ro/rw)
 677%           * default(all/fallback)
 678
 679rdfe_set_file_property(File, access(Access)) :-
 680    !,
 681    to_uri(File, URL),
 682    retractall(rdf_source_permission(URL, _)),
 683    assert(rdf_source_permission(URL, Access)),
 684    broadcast(rdf_file_property(URL, access(Access))).
 685rdfe_set_file_property(File, default(Type)) :-
 686    to_uri(File, URL),
 687    rdfe_set_file_property(URL, access(rw)), % must be writeable
 688    retractall(rdf_current_default_file(_,_)),
 689    assert(rdf_current_default_file(URL, Type)),
 690    broadcast(rdf_file_property(URL, default(Type))).
 691
 692
 693%!  rdfe_get_file_property(+FileOrURL, ?Option).
 694%!  rdfe_get_file_property(-URL, ?Option).
 695%
 696%   Fetch file properties set with rdfe_set_file_property/2.
 697
 698rdfe_get_file_property(FileOrURL, access(Access)) :-
 699    (   ground(FileOrURL)
 700    ->  to_uri(FileOrURL, URL)
 701    ;   rdf_source(_DB, URL),
 702        FileOrURL = URL
 703    ),
 704    (   rdf_source_permission(URL, Access0)
 705    ->  Access0 = Access
 706    ;   access_file(URL, write)
 707    ->  assert(rdf_source_permission(URL, rw)),
 708        Access = rw
 709    ;   assert(rdf_source_permission(URL, ro)),
 710        Access = ro
 711    ).
 712rdfe_get_file_property(FileOrURL, default(Default)) :-
 713    ground(FileOrURL),
 714    to_uri(FileOrURL, URL),
 715    (   rdf_current_default_file(URL, Default)
 716    ->  true
 717    ;   FileOrURL = user,
 718        Default = fallback
 719    ).
 720rdfe_get_file_property(URL, default(Default)) :-
 721    (   rdf_current_default_file(URL, Default)
 722    ->  true
 723    ;   URL = user,
 724        Default = fallback
 725    ).
 726
 727
 728%!  check_file_protection(-Error)
 729%
 730%   Check modification of all protected files
 731
 732check_file_protection(Error) :-
 733    (   rdfe_get_file_property(File, access(ro)),
 734        rdfe_is_modified(File)
 735    ->  Error = error(permission_error(modify, source, File), triple20)
 736    ;   true
 737    ).
 738
 739
 740%!  to_uri(+Spec, -URL) is det.
 741%
 742%   Convert a specification into a URL.
 743
 744to_uri(URL, URL) :-
 745    uri_components(URL, Components),
 746    uri_data(scheme, Components, Scheme),
 747    nonvar(Scheme),
 748    uri_scheme(Scheme),
 749    !.
 750to_uri(File, URL) :-
 751    uri_file_name(URL, File).
 752
 753
 754uri_scheme(file).
 755uri_scheme(http).
 756uri_scheme(https).
 757uri_scheme(ftp).
 758uri_scheme(ftps).
 759
 760
 761                 /*******************************
 762                 *           MODIFIED           *
 763                 *******************************/
 764
 765%!  rdfe_is_modified(?Source)
 766%
 767%   True if facts have been added, deleted or updated that have
 768%   Source as `payload'.
 769
 770rdfe_is_modified(Source) :-
 771    rdf_source(Graph, Source),
 772    rdf_graph_property(Graph, modified(true)).
 773
 774
 775rdfe_clear_modified :-
 776    forall(rdf_graph(File),
 777           rdfe_clear_modified(File)).
 778
 779%!  rdfe_clear_modified(+Graph) is det.
 780%
 781%   Consider the current state of Graph as _unmodified_.
 782
 783rdfe_clear_modified(Graph) :-
 784    rdf_set_graph(Graph, modified(false)).
 785
 786
 787                 /*******************************
 788                 *           WATERMARKS         *
 789                 *******************************/
 790
 791%!  rdfe_set_watermark(Name)
 792%
 793%   Create a watermark for undo and replay journal upto this point.
 794%   The rest of the logic needs to be written later.
 795
 796rdfe_set_watermark(Name) :-
 797    rdfe_current_transaction(TID),
 798    assert_action(TID, watermark(Name), -, -, -),
 799    journal(watermark(TID, Name)).
 800
 801
 802                 /*******************************
 803                 *             RESET            *
 804                 *******************************/
 805
 806%!  rdfe_reset
 807%
 808%   Clear database, undo, namespaces and journalling info.
 809
 810rdfe_reset :-
 811    rdfe_reset_journal,
 812    rdfe_reset_ns,
 813    rdfe_reset_undo,
 814    rdf_reset_db,
 815    broadcast(rdf_reset).
 816
 817%!  rdfe_reset_journal
 818%
 819%   If a journal is open, close it using rdfe_close_journal/0
 820
 821rdfe_reset_journal :-
 822    (   rdfe_current_journal(_)
 823    ->  rdfe_close_journal
 824    ;   true
 825    ).
 826
 827rdfe_reset_undo :-
 828    retractall(undo_log(_,_,_,_,_)),
 829    retractall(current_transaction(_)),
 830    retractall(transaction_name(_,_)),
 831    retractall(undo_marker(_,_)),
 832    retractall(snapshot_file(_)).
 833
 834%       close possible open journal at exit.  Using a Prolog hook
 835%       guarantees closure, even for most crashes.
 836
 837:- at_halt(rdfe_reset_journal).
 838
 839
 840                 /*******************************
 841                 *          JOURNALLING         *
 842                 *******************************/
 843
 844journal_version(1).
 845
 846%!  rdfe_open_journal(+File, +Mode) is det.
 847%
 848%   Open a journal writing to File in Mode.  Mode is one of
 849%
 850%           * read
 851%           Open and replay the journal
 852%
 853%           * write
 854%           Delete current journal and create a fresh one
 855%
 856%           * append
 857%           Read and replay the existing journal and append new
 858%           modifications to the File.
 859
 860rdfe_open_journal(_, _) :-              % already open
 861    journal(_, _, _),
 862    !.
 863rdfe_open_journal(File, read) :-
 864    !,
 865    absolute_file_name(File,
 866                       [ extensions([rdfj, '']),
 867                         access(read)
 868                       ],
 869                       Path),
 870    rdfe_replay_journal(Path),
 871    rdfe_clear_modified.
 872rdfe_open_journal(File, write) :-
 873    !,
 874    absolute_file_name(File,
 875                       [ extensions([rdfj, '']),
 876                         access(write)
 877                       ],
 878                       Path),
 879    open(Path, write, Stream, [close_on_abort(false)]),
 880    assert(journal(Path, write, Stream)),
 881    get_time(T),
 882    journal_open(start, T).
 883rdfe_open_journal(File, append) :-
 884    working_directory(CWD, CWD),
 885    absolute_file_name(File,
 886                       [ extensions([rdfj, '']),
 887                         relative_to(CWD),
 888                         access(write)
 889                       ],
 890                       Path),
 891    (   exists_file(Path)
 892    ->  rdfe_replay_journal(Path),
 893        rdfe_clear_modified,
 894        get_time(T),
 895        assert(journal(Path, append(T), []))
 896    ;   rdfe_open_journal(Path, write)
 897    ).
 898
 899
 900journal_open(Type, Time) :-
 901    journal_comment(Type, Time),
 902    SecTime is round(Time),
 903    journal_version(Version),
 904    Start =.. [ Type, [ time(SecTime),
 905                        version(Version)
 906                      ]
 907              ],
 908    journal(Start),
 909    broadcast(rdf_journal(Start)).
 910
 911journal_comment(start, Time) :-
 912    journal(_, _, Stream),
 913    format_time(string(String), '%+', Time),
 914    format(Stream,
 915           '/* Triple20 Journal File\n\n   \c
 916               Created: ~w\n   \c
 917               Triple20 by Jan Wielemaker <wielemak@science.uva.nl>\n\n   \c
 918               EDIT WITH CARE!\n\c
 919               */~n~n', [String]).
 920journal_comment(resume, Time) :-
 921    journal(_, _, Stream),
 922    format_time(string(String), '%+', Time),
 923    format(Stream,
 924           '\n\c
 925               /* Resumed: ~w\n\c
 926               */~n~n', [String]).
 927
 928%!  rdfe_close_journal
 929%
 930%   Close  the  journal.  Automatically  called    from  at  program
 931%   termination from at_halt/1.
 932
 933rdfe_close_journal :-
 934    get_time(T),
 935    SecTime is round(T),
 936    journal(end([ time(SecTime)
 937                ])),
 938    retract(journal(_, Mode, Stream)),
 939    (   Mode = append(_)
 940    ->  true
 941    ;   close(Stream)
 942    ).
 943
 944%!  rdfe_current_journal(-Path)
 945%
 946%   Query the currently open journal
 947
 948rdfe_current_journal(Path) :-
 949    journal(Path, _Mode, _Stream).
 950
 951journal(Term) :-
 952    journal(Path, append(T), _),
 953    !,
 954    (   Term = end(_)
 955    ->  true
 956    ;   open(Path, append, Stream, [close_on_abort(false)]),
 957        retractall(journal(Path, _, _)),
 958        assert(journal(Path, append, Stream)),
 959        journal_open(resume, T),
 960        journal(Term)
 961    ).
 962journal(Term) :-
 963    (   journal(_, _, Stream)
 964    ->  write_journal(Term, Stream),
 965        flush_output(Stream)
 966    ;   broadcast(rdf_no_journal(Term))
 967    ).
 968
 969write_journal(commit(TID, Time), Stream) :-
 970    !,
 971    format(Stream, 'commit(~q, ~2f).~n~n', [TID, Time]).
 972write_journal(Term, Stream) :-
 973    format(Stream, '~q.~n', [Term]).
 974
 975
 976%!  rdfe_replay_journal(+File)
 977%
 978%   Replay a journal file. For now  this   is  our cheap way to deal
 979%   with save/load. Future versions may be  more clever when dealing
 980%   with the version information stored in the journal.
 981
 982rdfe_replay_journal(File) :-
 983    absolute_file_name(File,
 984                       [ extensions([rdfj, '']),
 985                         access(read)
 986                       ],
 987                       Path),
 988    open(Path, read, Stream),
 989    replay(Stream),
 990    close(Stream).
 991
 992replay(Stream) :-
 993    read(Stream, Term),
 994    replay(Term, Stream).
 995
 996replay(end_of_file, _) :- !.
 997replay(start(_Attributes), Stream) :-
 998    !,
 999    read(Stream, Term),
1000    replay(Term, Stream).
1001replay(resume(_Attributes), Stream) :-
1002    !,
1003    read(Stream, Term),
1004    replay(Term, Stream).
1005replay(end(_Attributes), Stream) :-
1006    !,
1007    read(Stream, Term),
1008    replay(Term, Stream).
1009replay(Term0, Stream) :-
1010    replay_transaction(Term0, Stream),
1011    read(Stream, Term),
1012    replay(Term, Stream).
1013
1014replay_transaction(Term0, Stream) :-
1015    collect_transaction(Term0, Stream, Transaction, Last),
1016    (   committed_transaction(Last)
1017    ->  replay_actions(Transaction)
1018    ;   true
1019    ).
1020
1021collect_transaction(End, _, [], End) :-
1022    ends_transaction(End),
1023    !.
1024collect_transaction(A, Stream, [A|T], End) :-
1025    read(Stream, Term),
1026    collect_transaction(Term, Stream, T, End).
1027
1028committed_transaction(commit(_)).
1029committed_transaction(commit(_, _)).
1030
1031ends_transaction(end_of_file).
1032ends_transaction(commit(_)).
1033ends_transaction(commit(_, _)).
1034ends_transaction(rollback(_)).
1035ends_transaction(end(_)).
1036ends_transaction(start(_)).
1037
1038replay_actions([]).
1039replay_actions([H|T]) :-
1040    (   replay_action(H)
1041    ->  replay_actions(T)
1042    ;   print_message(warning,
1043                      rdf_replay_failed(H)),
1044        (   debugging(journal)
1045        ->  gtrace,
1046            replay_actions([H|T])
1047        ;   replay_actions(T)
1048        )
1049    ).
1050
1051
1052%!  replay_action(+Action)
1053%
1054%   Replay actions from the journal. Tricky is rdf_load/3. It should
1055%   reload the file in the state it  was   in  at  the moment it was
1056%   created. For now this has been hacked  for files that were empry
1057%   at the moment they where loaded (e.g. created from `new_file' in
1058%   our GUI prototype). How to solve this? We could warn if the file
1059%   appears changed, but this isn't really   easy  as copying and OS
1060%   differences makes it hard to decide on changes by length as well
1061%   as modification time. Alternatively we could   save the state in
1062%   seperate quick-load states.
1063
1064replay_action(retract(_, Subject, Predicate, Object, PayLoad)) :-
1065    rdf_retractall(Subject, Predicate, Object, PayLoad).
1066replay_action(assert(_, Subject, Predicate, Object, PayLoad)) :-
1067    rdf_assert(Subject, Predicate, Object, PayLoad).
1068replay_action(update(_, Subject, Predicate, Object, Action)) :-
1069    rdf_update(Subject, Predicate, Object, Action).
1070replay_action(update(_, Subject, Predicate, Object, Payload, Action)) :-
1071    rdf_update(Subject, Predicate, Object, Payload, Action).
1072replay_action(rdf_load(_, File, Options)) :-
1073    memberchk(md5(MD5), Options),
1074    snapshot_file(File, MD5,
1075                  [ access(read),
1076                    file_errors(fail)
1077                  ],
1078                  Path),
1079    !,
1080    debug(snapshot, 'Reloading snapshot ~w~n', [Path]),
1081    load_snapshot(File, Path).
1082replay_action(rdf_load(_, File, Options)) :-
1083    find_file(File, Options, Path),
1084    (   memberchk(triples(0), Options),
1085        memberchk(modified(Modified), Options)
1086    ->  rdf_retractall(_,_,_,Path:_),
1087        retractall(rdf_db:rdf_source(Path, _, _, _)),       % TBD: move
1088        rdf_md5(Path, MD5),
1089        assert(rdf_db:rdf_source(Path, Modified, 0, MD5))
1090    ;   rdf_load(Path)
1091    ).
1092replay_action(rdf_unload(_, Source)) :-
1093    rdf_unload(Source).
1094replay_action(ns(_, register(ID, URI))) :-
1095    !,
1096    rdf_register_ns(ID, URI).
1097replay_action(ns(_, unregister(ID, URI))) :-
1098    retractall(rdf_db:ns(ID, URI)).
1099replay_action(watermark(_, _Name)) :-
1100    true.
1101
1102find_file(File, _, File) :-
1103    exists_file(File),
1104    !.
1105find_file(File, Options, Path) :-
1106    memberchk(pwd(PWD), Options),
1107    make_path(File, PWD, Path),
1108    exists_file(Path),
1109    !.
1110
1111%!  make_path(+File, +PWD, -Path)
1112%
1113%   Return location of File relative to PWD, Parent of PWD, etc. (TBD)
1114
1115make_path(File, PWD, Path) :-
1116    atom_concat(PWD, /, PWD2),
1117    atom_concat(PWD2, Path, File).
1118
1119
1120                 /*******************************
1121                 *            MESSAGES          *
1122                 *******************************/
1123
1124:- multifile
1125    prolog:message/3,
1126    user:message_hook/3.
1127
1128%       Catch messages.
1129
1130prolog:message(rdf_replay_failed(Term)) -->
1131    [ 'RDFDB: Replay of ~p failed'-[Term] ].
1132prolog:message(rdf_undo_failed(Term)) -->
1133    [ 'RDFDB: Undo of ~p failed'-[Term] ].