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):
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
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):
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