Package Gnumed :: Package timelinelib :: Package db :: Package backends :: Module xmlfile |
[frames] | no frames]

# Source Code for Module Gnumed.timelinelib.db.backends.xmlfile

      1  # Copyright (C) 2009, 2010, 2011  Rickard Lindberg, Roger Lindberg
      2  #
      3  # This file is part of Timeline.
      4  #
      5  # Timeline is free software: you can redistribute it and/or modify
      6  # it under the terms of the GNU General Public License as published by
      7  # the Free Software Foundation, either version 3 of the License, or
      8  # (at your option) any later version.
      9  #
     10  # Timeline is distributed in the hope that it will be useful,
     11  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13  # GNU General Public License for more details.
     14  #
     15  # You should have received a copy of the GNU General Public License
     16  # along with Timeline.  If not, see <http://www.gnu.org/licenses/>.
     17
     18
     19  """
     20  Implementation of timeline database with xml file storage.
     21  """
     22
     23
     24  import re
     25  import os.path
     26  from os.path import abspath
     27  import base64
     28  import StringIO
     29  from xml.sax.saxutils import escape as xmlescape
     30
     31  import wx
     32
     33  from timelinelib.db.backends.memory import MemoryDB
     34  from timelinelib.db.exceptions import TimelineIOError
     35  from timelinelib.db.objects import Category
     36  from timelinelib.db.objects import Container
     37  from timelinelib.db.objects import Event
     38  from timelinelib.db.objects import Subevent
     39  from timelinelib.db.objects import TimePeriod
     40  from timelinelib.db.utils import safe_write
     41  from timelinelib.meta.version import get_version
     42  from timelinelib.time import WxTimeType
     43  from timelinelib.utils import ex_msg
     44  from timelinelib.xml.parser import ANY
     45  from timelinelib.xml.parser import OPTIONAL
     46  from timelinelib.xml.parser import parse
     47  from timelinelib.xml.parser import parse_fn_store
     48  from timelinelib.xml.parser import SINGLE
     49  from timelinelib.xml.parser import Tag
     50
     51
     52  ENCODING = "utf-8"
     53  INDENT1 = "  "
     54  INDENT2 = "    "
     55  INDENT3 = "      "
     56
     57
     58  # Must be defined before the XmlTimeline class since it is used as a decorator
     59 -def wrap_in_tag(func, name, indent=""):
     60      def wrapper(*args, **kwargs):
     61          file = args[1] # 1st argument is self, 2nd argument is file
     62          file.write(indent)
     63          file.write("<")
     64          file.write(name)
     65          file.write(">\n")
     66          func(*args, **kwargs)
     67          file.write(indent)
     68          file.write("</")
     69          file.write(name)
     70          file.write(">\n")
     71      return wrapper
     72
     73
     74 -class ParseException(Exception):
     75      """Thrown if parsing of data read from file fails."""
     76      pass
     77
     78
     79 -class XmlTimeline(MemoryDB):
     80
     81 -    def __init__(self, path, load=True, use_wide_date_range=False):
     82          MemoryDB.__init__(self)
     83          self.path = path
     84          self._set_time_type(use_wide_date_range)
     85          if load == True:
     86              self._load()
     87              self._fill_containers()
     88
     89 -    def _set_time_type(self, use_wide_date_range):
     90          if use_wide_date_range == True:
     91              self.time_type = WxTimeType()
     92
     93 -    def _parse_time(self, time_string):
     94          return self.get_time_type().parse_time(time_string)
     95
     96 -    def _time_string(self, time):
     97          return self.get_time_type().time_string(time)
     98
     99 -    def _fill_containers(self):
    100          container_events = [event for event in self.events
    101                              if event.is_container()]
    102          subevents = [event for event in self.events
    103                       if event.is_subevent()]
    104          containers = {}
    105          for container in container_events:
    106              containers[container.cid()] = container
    107          for subevent in subevents:
    108              try:
    109                  container = containers[subevent.cid()]
    110                  container.register_subevent(subevent)
    111              except:
    112                  #TODO: Create container
    113                  pass
    114
    115 -    def _load(self):
    116          """
    117          Load timeline data from the file that this timeline points to.
    118
    119          This should only be done once when this class is created.
    120
    121          The data is stored internally until we do a save.
    122
    123          If a read error occurs a TimelineIOError will be raised.
    124          """
    125          if not os.path.exists(self.path):
    126              # Nothing to load. Will create a new timeline on save.
    127              return
    128          try:
    129              # _parse_version will create the rest of the schema dynamically
    130              partial_schema = Tag("timeline", SINGLE, None, [
    131                  Tag("version", SINGLE, self._parse_version)
    132              ])
    133              tmp_dict = {
    134                  "partial_schema": partial_schema,
    135                  "category_map": {},
    136                  "hidden_categories": [],
    137              }
    138              self.disable_save()
    139              parse(self.path, partial_schema, tmp_dict)
    140              self.enable_save(call_save=False)
    141          except Exception, e:
    142              msg = _("Unable to read timeline data from '%s'.")
    143              whole_msg = (msg + "\n\n%s") % (abspath(self.path), ex_msg(e))
    144              raise TimelineIOError(whole_msg)
    145
    146 -    def _parse_version(self, text, tmp_dict):
    147          match = re.search(r"^(\d+).(\d+).(\d+)(dev.*)?$", text)
    148          if match:
    149              (x, y, z) = (int(match.group(1)), int(match.group(2)),
    150                           int(match.group(3)))
    151              tmp_dict["version"] = (x, y, z)
    152              self._create_rest_of_schema(tmp_dict)
    153          else:
    154              raise ParseException("Could not parse version number from '%s'."
    155                                   % text)
    156
    157 -    def _create_rest_of_schema(self, tmp_dict):
    158          """
    159          Ensure all versions of the xml format can be parsed with this schema.
    160
    161          tmp_dict["version"] can be used to create different schemas depending
    162          on the version.
    163          """
    164          tmp_dict["partial_schema"].add_child_tags([
    165              Tag("categories", SINGLE, None, [
    166                  Tag("category", ANY, self._parse_category, [
    167                      Tag("name", SINGLE, parse_fn_store("tmp_name")),
    168                      Tag("color", SINGLE, parse_fn_store("tmp_color")),
    169                      Tag("font_color", OPTIONAL, parse_fn_store("tmp_font_color")),
    170                      Tag("parent", OPTIONAL, parse_fn_store("tmp_parent")),
    171                  ])
    172              ]),
    173              Tag("events", SINGLE, None, [
    174                  Tag("event", ANY, self._parse_event, [
    175                      Tag("start", SINGLE, parse_fn_store("tmp_start")),
    176                      Tag("end", SINGLE, parse_fn_store("tmp_end")),
    177                      Tag("text", SINGLE, parse_fn_store("tmp_text")),
    178                      Tag("fuzzy", OPTIONAL, parse_fn_store("tmp_fuzzy")),
    179                      Tag("locked", OPTIONAL, parse_fn_store("tmp_locked")),
    180                      Tag("ends_today", OPTIONAL, parse_fn_store("tmp_ends_today")),
    181                      Tag("category", OPTIONAL,
    182                          parse_fn_store("tmp_category")),
    183                      Tag("description", OPTIONAL,
    184                          parse_fn_store("tmp_description")),
    185                      Tag("alert", OPTIONAL,
    186                          parse_fn_store("tmp_alert")),
    187                      Tag("hyperlink", OPTIONAL,
    188                          parse_fn_store("tmp_hyperlink")),
    189                      Tag("icon", OPTIONAL,
    190                          parse_fn_store("tmp_icon")),
    191                  ])
    192              ]),
    193              Tag("view", SINGLE, None, [
    194                  Tag("displayed_period", OPTIONAL,
    195                      self._parse_displayed_period, [
    196                      Tag("start", SINGLE, parse_fn_store("tmp_start")),
    197                      Tag("end", SINGLE, parse_fn_store("tmp_end")),
    198                  ]),
    199                  Tag("hidden_categories", OPTIONAL,
    200                      self._parse_hidden_categories, [
    201                      Tag("name", ANY, self._parse_hidden_category),
    202                  ]),
    203              ]),
    204          ])
    205
    206 -    def _parse_category(self, text, tmp_dict):
    207          name = tmp_dict.pop("tmp_name")
    208          color = parse_color(tmp_dict.pop("tmp_color"))
    209          font_color = self._parse_optional_color(tmp_dict, "tmp_font_color")
    210          parent_name = tmp_dict.pop("tmp_parent", None)
    211          if parent_name:
    212              parent = tmp_dict["category_map"].get(parent_name, None)
    213              if parent is None:
    214                  raise ParseException("Parent category '%s' not found." % parent_name)
    215          else:
    216              parent = None
    217          category = Category(name, color, font_color, True, parent=parent)
    218          tmp_dict["category_map"][name] = category
    219          self.save_category(category)
    220
    221 -    def _parse_event(self, text, tmp_dict):
    222          start = self._parse_time(tmp_dict.pop("tmp_start"))
    223          end = self._parse_time(tmp_dict.pop("tmp_end"))
    224          text = tmp_dict.pop("tmp_text")
    225          fuzzy = self._parse_optional_bool(tmp_dict, "tmp_fuzzy")
    226          locked = self._parse_optional_bool(tmp_dict, "tmp_locked")
    227          ends_today = self._parse_optional_bool(tmp_dict, "tmp_ends_today")
    228          category_text = tmp_dict.pop("tmp_category", None)
    229          if category_text is None:
    230              category = None
    231          else:
    232              category = tmp_dict["category_map"].get(category_text, None)
    233              if category is None:
    234                  raise ParseException("Category '%s' not found." % category_text)
    235          description = tmp_dict.pop("tmp_description", None)
    236          alert_string = tmp_dict.pop("tmp_alert", None)
    237          alert = self._parse_alert_string(alert_string)
    238          icon_text = tmp_dict.pop("tmp_icon", None)
    239          if icon_text is None:
    240              icon = None
    241          else:
    242              icon = parse_icon(icon_text)
    243          hyperlink = tmp_dict.pop("tmp_hyperlink", None)
    244          if self._is_container_event(text):
    245              cid, text = self._extract_container_id(text)
    246              event = Container(self.get_time_type(), start, end, text, category, cid=cid)
    247          elif self._is_subevent(text):
    248              cid, text = self._extract_subid(text)
    249              event = Subevent(self.get_time_type(), start, end, text, category, cid=cid)
    250          else:
    251              event = Event(self.get_time_type(), start, end, text, category, fuzzy, locked, ends_today)
    252          event.set_data("description", description)
    253          event.set_data("icon", icon)
    254          event.set_data("alert", alert)
    255          event.set_data("hyperlink", hyperlink)
    256          self.save_event(event)
    257
    258 -    def alert_string(self, alert):
    259          time, text = alert
    260          time_string = self._time_string(time)
    261          return "%s;%s" % (time_string, text)
    262
    263 -    def _parse_alert_string(self, alert_string):
    264          if alert_string is not None:
    265              try:
    266                  time_string, alert_text = alert_string.split(";", 1)
    267                  alert_time = self._parse_time(time_string)
    268                  alert = (alert_time, alert_text)
    269              except:
    270                  raise ParseException("Could not parse alert from '%s'." % alert_string)
    271          else:
    272              alert = None
    273          return alert
    274
    275 -    def _is_container_event(self, text):
    276          return text.startswith("[")
    277
    278 -    def _is_subevent(self, text):
    279          return text.startswith("(")
    280
    281 -    def _extract_container_id(self, text):
    282          str_id, text = text.split("]", 1)
    283          try:
    284              str_id = str_id[1:]
    285              id = int(str_id)
    286          except:
    287              id = -1
    288          return id, text
    289
    290 -    def _extract_subid(self, text):
    291          id, text = text.split(")", 1)
    292          try:
    293              id = int(id[1:])
    294          except:
    295              id = -1
    296          return id, text
    297
    298 -    def _parse_optional_bool(self, tmp_dict, id):
    299          if tmp_dict.has_key(id):
    300              return tmp_dict.pop(id) == "True"
    301          else:
    302              return False
    303
    304 -    def _parse_optional_color(self, tmp_dict, id):
    305          if tmp_dict.has_key(id):
    306              return parse_color(tmp_dict.pop(id))
    307          else:
    308              return (0, 0, 0)
    309
    310 -    def _parse_displayed_period(self, text, tmp_dict):
    311          start = self._parse_time(tmp_dict.pop("tmp_start"))
    312          end = self._parse_time(tmp_dict.pop("tmp_end"))
    313          self._set_displayed_period(TimePeriod(self.get_time_type(), start, end))
    314
    315 -    def _parse_hidden_category(self, text, tmp_dict):
    316          category = tmp_dict["category_map"].get(text, None)
    317          if category is None:
    318              raise ParseException("Category '%s' not found." % text)
    319          tmp_dict["hidden_categories"].append(category)
    320
    321 -    def _parse_hidden_categories(self, text, tmp_dict):
    322          self._set_hidden_categories(tmp_dict.pop("hidden_categories"))
    323
    324 -    def _save(self):
    325          self._make_sure_subevets_are_saved_last()
    326          safe_write(self.path, ENCODING, self._write_xml_doc)
    327
    328 -    def _make_sure_subevets_are_saved_last(self):
    329          subevents = [event for event in self.events if event.is_subevent()]
    330          events = [event for event in self.events if not event.is_subevent()]
    331          events.extend(subevents)
    332          self.events = events
    333
    334 -    def _write_xml_doc(self, file):
    335          file.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
    336          self._write_timeline(file)
    337
    338 -    def _write_timeline(self, file):
    339          write_simple_tag(file, "version", get_version(), INDENT1)
    340          self._write_categories(file)
    341          self._write_events(file)
    342          self._write_view(file)
    343      _write_timeline = wrap_in_tag(_write_timeline, "timeline")
    344
    345 -    def _write_categories(self, file):
    346          def write_with_parent(categories, parent):
    347              for cat in categories:
    348                  if cat.parent == parent:
    349                      self._write_category(file, cat)
    350                      write_with_parent(categories, cat)
    351          write_with_parent(self.get_categories(), None)
    352      _write_categories = wrap_in_tag(_write_categories, "categories", INDENT1)
    353
    354 -    def _write_category(self, file, cat):
    355          write_simple_tag(file, "name", cat.name, INDENT3)
    356          write_simple_tag(file, "color", color_string(cat.color), INDENT3)
    357          write_simple_tag(file, "font_color", color_string(cat.font_color), INDENT3)
    358          if cat.parent:
    359              write_simple_tag(file, "parent", cat.parent.name, INDENT3)
    360      _write_category = wrap_in_tag(_write_category, "category", INDENT2)
    361
    362 -    def _write_events(self, file):
    363          for evt in self.get_all_events():
    364              self._write_event(file, evt)
    365      _write_events = wrap_in_tag(_write_events, "events", INDENT1)
    366
    367 -    def _write_event(self, file, evt):
    368          write_simple_tag(file, "start",
    369                           self._time_string(evt.time_period.start_time), INDENT3)
    370          write_simple_tag(file, "end",
    371                           self._time_string(evt.time_period.end_time), INDENT3)
    372          if evt.is_container():
    373              write_simple_tag(file, "text", "[%d]%s " % (evt.cid(), evt.text), INDENT3)
    374          elif evt.is_subevent():
    375              write_simple_tag(file, "text", "(%d)%s " % (evt.cid(), evt.text), INDENT3)
    376          else:
    377              write_simple_tag(file, "text", evt.text, INDENT3)
    378          write_simple_tag(file, "fuzzy", "%s" % evt.fuzzy, INDENT3)
    379          write_simple_tag(file, "locked", "%s" % evt.locked, INDENT3)
    380          write_simple_tag(file, "ends_today", "%s" % evt.ends_today, INDENT3)
    381          if evt.category is not None:
    382              write_simple_tag(file, "category", evt.category.name, INDENT3)
    383          if evt.get_data("description") is not None:
    384              write_simple_tag(file, "description", evt.get_data("description"),
    385                               INDENT3)
    386          alert = evt.get_data("alert")
    387          if alert is not None:
    388              write_simple_tag(file, "alert", self.alert_string(alert),
    389                               INDENT3)
    390          hyperlink = evt.get_data("hyperlink")
    391          if hyperlink is not None:
    392              write_simple_tag(file, "hyperlink", hyperlink, INDENT3)
    393          if evt.get_data("icon") is not None:
    394              icon_text = icon_string(evt.get_data("icon"))
    395              write_simple_tag(file, "icon", icon_text, INDENT3)
    396      _write_event = wrap_in_tag(_write_event, "event", INDENT2)
    397
    398 -    def _write_view(self, file):
    399          if self._get_displayed_period() is not None:
    400              self._write_displayed_period(file)
    401          self._write_hidden_categories(file)
    402      _write_view = wrap_in_tag(_write_view, "view", INDENT1)
    403
    404 -    def _write_displayed_period(self, file):
    405          period = self._get_displayed_period()
    406          write_simple_tag(file, "start",
    407                           self._time_string(period.start_time), INDENT3)
    408          write_simple_tag(file, "end",
    409                           self._time_string(period.end_time), INDENT3)
    410      _write_displayed_period = wrap_in_tag(_write_displayed_period,
    411                                            "displayed_period", INDENT2)
    412
    413 -    def _write_hidden_categories(self, file):
    414          for cat in self._get_hidden_categories():
    415              write_simple_tag(file, "name", cat.name, INDENT3)
    416      _write_hidden_categories = wrap_in_tag(_write_hidden_categories,
    417                                             "hidden_categories", INDENT2)
    418
    419
    420 -def write_simple_tag(file, name, content, indent=""):
    421      file.write(indent)
    422      file.write("<")
    423      file.write(name)
    424      file.write(">")
    425      file.write(xmlescape(content))
    426      file.write("</")
    427      file.write(name)
    428      file.write(">\n")
    429
    430
    431 -def parse_bool(bool_string):
    432      """
    433      Expected format 'True' or 'False'.
    434
    435      Return True or False.
    436      """
    437      if bool_string == "True":
    438          return True
    439      elif bool_string == "False":
    440          return False
    441      else:
    442          raise ParseException("Unknown boolean '%s'" % bool_string)
    443
    444
    445 -def color_string(color):
    446      return "%i,%i,%i" % color
    447
    448
    449 -def parse_color(color_string):
    450      """
    451      Expected format 'r,g,b'.
    452
    453      Return a tuple (r, g, b).
    454      """
    455      def verify_255_number(num):
    456          if num < 0 or num > 255:
    457              raise ParseException("Color number not in range [0, 255], "
    458                                   "color string = '%s'" % color_string)
    459      match = re.search(r"^(\d+),(\d+),(\d+)$", color_string)
    460      if match:
    461          r, g, b = int(match.group(1)), int(match.group(2)), int(match.group(3))
    462          verify_255_number(r)
    463          verify_255_number(g)
    464          verify_255_number(b)
    465          return (r, g, b)
    466      else:
    467          raise ParseException("Color not on correct format, color string = '%s'"
    468                               % color_string)
    469
    470 -def icon_string(bitmap):
    471      output = StringIO.StringIO()
    472      image = wx.ImageFromBitmap(bitmap)
    473      image.SaveStream(output, wx.BITMAP_TYPE_PNG)
    474      return base64.b64encode(output.getvalue())
    475
    476
    477 -def parse_icon(string):
    478      """
    479      Expected format: base64 encoded png image.
    480
    481      Return a wx.Bitmap.
    482      """
    483      try:
    484          input = StringIO.StringIO(base64.b64decode(string))
    485          image = wx.ImageFromStream(input, wx.BITMAP_TYPE_PNG)
    486          return image.ConvertToBitmap()
    487      except:
    488          raise ParseException("Could not parse icon from '%s'." % string)
    489