summaryrefslogtreecommitdiff
path: root/src/library/scala/xml/persistent/CachedFileStorage.scala
blob: 923e8c255754383d5429ad074fd7633a72f27191 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/*                     __                                               *\
**     ________ ___   / /  ___     Scala API                            **
**    / __/ __// _ | / /  / _ |    (c) 2002-2009, LAMP/EPFL             **
**  __\ \/ /__/ __ |/ /__/ __ |    http://scala-lang.org/               **
** /____/\___/_/ |_/____/_/ | |                                         **
**                          |/                                          **
\*                                                                      */

// $Id$

package scala.xml
package persistent

import java.io.{File, FileOutputStream}
import java.nio.ByteBuffer
import java.nio.channels.Channels

/** <p>
 *    Mutable storage of immutable xml trees. Everything is kept in memory,
 *    with a thread periodically checking for changes and writing to file.
 *    To ensure atomicity, two files are used, filename1 and '$'+filename1.
 *    The implementation switches between the two, deleting the older one
 *    after a complete dump of the database has been written.
 *  </p>
 *
 *  @author Burak Emir
 */
abstract class CachedFileStorage(private val file1: File)
extends java.lang.Thread with scala.util.logging.Logged {

  private val file2 = new File(file1.getParent, file1.getName+"$")

  /**  either equals file1 or file2, references the next file in which updates will be stored
   */
  private var theFile: File = null

  private def switch = { theFile = if (theFile == file1) file2 else file1; }

  /** this storage modified since last modification check */
  protected var dirty = false

  /** period between modification checks, in milliseconds */
  protected val interval = 1000

  /** finds and loads the storage file. subclasses should call this method
   *  prior to any other, but only once, to obtain the initial sequence of nodes.
   */
  protected def initialNodes: Iterator[Node] = (file1.exists, file2.exists) match {
    case (false,false) =>
      theFile = file1
      Iterator.empty
    case (true, true ) if (file1.lastModified < file2.lastModified) =>
      theFile = file2
      load
    case (true, _ ) =>
      theFile = file1
      load
    case _ =>
      theFile = file2
      load
  }

  /** returns an iterator over the nodes in this storage */
  def nodes: Iterator[Node]

  /** adds a node, setting this.dirty to true as a side effect */
  def += (e: Node): Unit

  /** removes a tree, setting this.dirty to true as a side effect */
  def -= (e: Node): Unit

  /* loads and parses XML from file */
  private def load: Iterator[Node] = {
    import scala.io.Source
    import scala.xml.parsing.ConstructingParser
    log("[load]\nloading "+theFile)
    val src = Source.fromFile(theFile)()
    log("parsing "+theFile)
    val res = ConstructingParser.fromSource(src,false).document.docElem(0)
    switch
    log("[load done]")
    res.child.iterator
  }

  /** saves the XML to file */
  private def save = if (this.dirty) {
    log("[save]\ndeleting "+theFile);
    theFile.delete();
    log("creating new "+theFile);
    theFile.createNewFile();
    val fos = new FileOutputStream(theFile)
    val c   = fos.getChannel()

    // @todo: optimize
    val storageNode = <nodes>{ nodes.toList }</nodes>
    val w = Channels.newWriter(c, "utf-8")
    XML.write(w, storageNode, "utf-8", true, null)

    log("writing to "+theFile);

    w.close
    c.close
    fos.close
    dirty = false
    switch
    log("[save done]")
  }

  /** run method of the thread. remember to use start() to start a thread, not run. */
  override def run = {
    log("[run]\nstarting storage thread, checking every "+interval+" ms");
    while(true) {
      Thread.sleep( this.interval );
      save
    }
  }

  /** forces writing of contents to the file, even if there has not been any update. */
  def flush = {
    this.dirty = true;
    save
  }
}