Added support for loading XML file even if not following XSD schema (but still following XML syntax), warning user of errors in XML file
authorLaurent Bessard
Fri, 27 Sep 2013 16:22:40 +0200
changeset 1330 96b242e4c59d
parent 1328 a2f2981df9b0
child 1331 38c5de794e62
Added support for loading XML file even if not following XSD schema (but still following XML syntax), warning user of errors in XML file
CodeFileTreeNode.py
ConfigTreeNode.py
PLCControler.py
PLCOpenEditor.py
ProjectController.py
plcopen/plcopen.py
plcopen/structures.py
py_ext/PythonFileCTNMixin.py
xmlclass/xmlclass.py
--- a/CodeFileTreeNode.py	Wed Sep 25 11:50:40 2013 +0200
+++ b/CodeFileTreeNode.py	Fri Sep 27 16:22:40 2013 +0200
@@ -1,10 +1,11 @@
-import os, re
+import os, re, traceback
 
 from copy import deepcopy
 from lxml import etree
 from xmlclass import GenerateParserFromXSDstring
 
 from PLCControler import UndoBuffer
+from ConfigTreeNode import XSDSchemaErrorMessage
 
 CODEFILE_XSD = """<?xml version="1.0" encoding="ISO-8859-1" ?>
 <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
@@ -84,9 +85,16 @@
                 (re.compile("(?<!<xhtml:p>)(?:<!\[CDATA\[)"), "<xhtml:p><![CDATA["),
                 (re.compile("(?:]]>)(?!</xhtml:p>)"), "]]></xhtml:p>")]:
                 codefile_xml = cre.sub(repl, codefile_xml)
-            self.CodeFile = etree.fromstring(codefile_xml, self.CodeFileParser)    
-            self.CreateCodeFileBuffer(True)
-        
+            
+            try:
+                self.CodeFile, error = self.CodeFileParser.LoadXMLString(codefile_xml)
+                if error is not None:
+                    self.GetCTRoot().logger.write_warning(
+                        XMLSyntaxErrorMessage % ((self.CODEFILE_NAME,) + error))
+                self.CreateCodeFileBuffer(True)
+            except Exception, exc:
+                self.GetCTRoot().logger.write_error(_("Couldn't load confnode parameters %s :\n %s") % (CTNName, unicode(exc)))
+                self.GetCTRoot().logger.write_error(traceback.format_exc())
         else:
             self.CodeFile = self.CodeFileParser.CreateRoot()
             self.CreateCodeFileBuffer(False)
--- a/ConfigTreeNode.py	Wed Sep 25 11:50:40 2013 +0200
+++ b/ConfigTreeNode.py	Fri Sep 27 16:22:40 2013 +0200
@@ -29,6 +29,7 @@
         </xsd:schema>""")
 
 NameTypeSeparator = '@'
+XSDSchemaErrorMessage = _("%s XML file doesn't follow XSD schema at line %d:\n%s")
 
 class ConfigTreeNode:
     """
@@ -585,8 +586,10 @@
         if self.MandatoryParams:
             try:
                 basexmlfile = open(self.ConfNodeBaseXmlFilePath(CTNName), 'r')
-                self.BaseParams = etree.fromstring(
-                    basexmlfile.read(), _BaseParamsParser)
+                self.BaseParams, error = _BaseParamsParser.LoadXMLString(basexmlfile.read())
+                if error is not None:
+                    self.GetCTRoot().logger.write_warning(
+                        XSDSchemaErrorMessage % ((CTNName + " BaseParams",) + error))
                 self.MandatoryParams = ("BaseParams", self.BaseParams)
                 basexmlfile.close()
             except Exception, exc:
@@ -597,7 +600,10 @@
         if self.CTNParams:
             try:
                 xmlfile = open(self.ConfNodeXmlFilePath(CTNName), 'r')
-                obj = etree.fromstring(xmlfile.read(), self.Parser)
+                obj, error = self.Parser.LoadXMLString(xmlfile.read())
+                if error is not None:
+                    self.GetCTRoot().logger.write_warning(
+                        XSDSchemaErrorMessage % ((CTNName,) + error))
                 name = obj.getLocalTag()
                 setattr(self, name, obj)
                 self.CTNParams = (name, obj)
--- a/PLCControler.py	Wed Sep 25 11:50:40 2013 +0200
+++ b/PLCControler.py	Fri Sep 27 16:22:40 2013 +0200
@@ -844,8 +844,10 @@
         Adds the POU defined by 'pou_xml' to the current project with type 'pou_type'
         '''
         try:
-            new_pou = LoadPou(pou_xml)
+            new_pou, error = LoadPou(pou_xml)
         except:
+            error = ""
+        if error is not None:
             return _("Couldn't paste non-POU object.")
         
         name = new_pou.getname()
@@ -2207,10 +2209,10 @@
             new_id = {}
             
             try:
-                instances = LoadPouInstances(text.encode("utf-8"), bodytype)
-                if len(instances) == 0:
-                    raise ValueError
+                instances, error = LoadPouInstances(text.encode("utf-8"), bodytype)
             except:
+                instances, error = [], ""
+            if error is not None or len(instances) == 0:
                 return _("Invalid plcopen element(s)!!!")
             
             exclude = {}
@@ -3063,10 +3065,9 @@
             return tasks_data, instances_data
 
     def OpenXMLFile(self, filepath):
-        #try:
-        self.Project = LoadProject(filepath)
-        #except Exception, e:
-        #    return _("Project file syntax error:\n\n") + str(e)
+        self.Project, error = LoadProject(filepath)
+        if self.Project is None:
+            return _("Project file syntax error:\n\n") + error
         self.SetFilePath(filepath)
         self.CreateProjectBuffer(True)
         self.ProgramChunks = []
@@ -3075,7 +3076,7 @@
         self.CurrentCompiledProject = None
         self.Buffering = False
         self.CurrentElementEditing = None
-        return None
+        return error
         
     def SaveXMLFile(self, filepath = None):
         if not filepath and self.FilePath == "":
--- a/PLCOpenEditor.py	Wed Sep 25 11:50:40 2013 +0200
+++ b/PLCOpenEditor.py	Fri Sep 27 16:22:40 2013 +0200
@@ -182,12 +182,11 @@
                 # Create a new controller
                 controler = PLCControler()
                 result = controler.OpenXMLFile(fileOpen)
-                if result is None:
-                    self.Controler = controler
-                    self.LibraryPanel.SetController(controler)
-                    self.ProjectTree.Enable(True)
-                    self.PouInstanceVariablesPanel.SetController(controler)
-                    self._Refresh(PROJECTTREE, POUINSTANCEVARIABLESPANEL, LIBRARYTREE)
+                self.Controler = controler
+                self.LibraryPanel.SetController(controler)
+                self.ProjectTree.Enable(True)
+                self.PouInstanceVariablesPanel.SetController(controler)
+                self._Refresh(PROJECTTREE, POUINSTANCEVARIABLESPANEL, LIBRARYTREE)
         
         # Define PLCOpenEditor icon
         self.SetIcon(wx.Icon(os.path.join(CWD, "images", "poe.ico"),wx.BITMAP_TYPE_ICO))
@@ -197,7 +196,8 @@
         self._Refresh(TITLE, EDITORTOOLBAR, FILEMENU, EDITMENU, DISPLAYMENU)
         
         if result is not None:
-            self.ShowErrorMessage(result)
+            self.ShowErrorMessage(
+                _("PLC syntax error at line %d:\n%s") % result)
 
     def OnCloseFrame(self, event):
         if self.Controler is None or self.CheckSaveBeforeClosing(_("Close Application")):
@@ -300,17 +300,17 @@
                 self.ResetView()
                 controler = PLCControler()
                 result = controler.OpenXMLFile(filepath)
-                if result is None:
-                    self.Controler = controler
-                    self.LibraryPanel.SetController(controler)
-                    self.ProjectTree.Enable(True)
-                    self.PouInstanceVariablesPanel.SetController(controler)
-                    self._Refresh(PROJECTTREE, LIBRARYTREE)
+                self.Controler = controler
+                self.LibraryPanel.SetController(controler)
+                self.ProjectTree.Enable(True)
+                self.PouInstanceVariablesPanel.SetController(controler)
+                self._Refresh(PROJECTTREE, LIBRARYTREE)
             self._Refresh(TITLE, EDITORTOOLBAR, FILEMENU, EDITMENU)
         dialog.Destroy()
         
         if result is not None:
-            self.ShowErrorMessage(result)
+            self.ShowErrorMessage(
+                _("PLC syntax error at line %d:\n%s") % result)
     
     def OnCloseProjectMenu(self, event):
         if not self.CheckSaveBeforeClosing():
--- a/ProjectController.py	Wed Sep 25 11:50:40 2013 +0200
+++ b/ProjectController.py	Fri Sep 27 16:22:40 2013 +0200
@@ -27,7 +27,7 @@
 from PLCControler import PLCControler
 from plcopen.structures import IEC_KEYWORDS
 from targets.typemapping import DebugTypesSize, LogLevelsCount, LogLevels
-from ConfigTreeNode import ConfigTreeNode
+from ConfigTreeNode import ConfigTreeNode, XSDSchemaErrorMessage
 
 base_folder = os.path.split(sys.path[0])[0]
 
@@ -318,9 +318,13 @@
         if not os.path.isfile(plc_file):
             return _("Chosen folder doesn't contain a program. It's not a valid project!")
         # Load PLCOpen file
-        result = self.OpenXMLFile(plc_file)
-        if result:
-            return result
+        error = self.OpenXMLFile(plc_file)
+        if error is not None:
+            if self.Project is not None:
+                self.logger.write_warning(
+                    XSDSchemaErrorMessage % (("PLC",) + error))
+            else:
+                return error
         if len(self.GetProjectConfigNames()) == 0:
             self.AddProjectDefaultConfiguration()
         # Change XSD into class members
--- a/plcopen/plcopen.py	Wed Sep 25 11:50:40 2013 +0200
+++ b/plcopen/plcopen.py	Fri Sep 27 16:22:40 2013 +0200
@@ -156,35 +156,44 @@
   </body>
 </pou>""" % locals()
 
-def LoadProject(filepath):
-    project_file = open(filepath)
-    project_xml = project_file.read().replace(
+def LoadProjectXML(project_xml):
+    project_xml = project_xml.replace(
         "http://www.plcopen.org/xml/tc6.xsd", 
         "http://www.plcopen.org/xml/tc6_0201")
     for cre, repl in [
         (re.compile("(?<!<xhtml:p>)(?:<!\[CDATA\[)"), "<xhtml:p><![CDATA["),
         (re.compile("(?:]]>)(?!</xhtml:p>)"), "]]></xhtml:p>")]:
         project_xml = cre.sub(repl, project_xml)
+    
+    try:
+        tree, error = PLCOpenParser.LoadXMLString(project_xml)
+        if error is not None:
+            # TODO Validate file according to PLCOpen v1 and modify it for
+            # compatibility with PLCOpen v2
+            return tree, error
+        return tree, None
+    except Exception, e:
+        return None, e.message
+
+def LoadProject(filepath):
+    project_file = open(filepath)
+    project_xml = project_file.read()
     project_file.close()
-    
-    return etree.fromstring(project_xml, PLCOpenParser)
+    return LoadProjectXML(project_xml)
 
 project_pou_xpath = PLCOpen_XPath("/ppx:project/ppx:types/ppx:pous/ppx:pou")
 def LoadPou(xml_string):
-    root = etree.fromstring(
-        LOAD_POU_PROJECT_TEMPLATE % xml_string, 
-        PLCOpenParser)
-    return project_pou_xpath(root)[0]
+    root, error = LoadProjectXML(LOAD_POU_PROJECT_TEMPLATE % xml_string)
+    return project_pou_xpath(root)[0], error
 
 project_pou_instances_xpath = {
     body_type: PLCOpen_XPath(
         "/ppx:project/ppx:types/ppx:pous/ppx:pou[@name='paste_pou']/ppx:body/ppx:%s/*" % body_type)
     for body_type in ["FBD", "LD", "SFC"]}
 def LoadPouInstances(xml_string, body_type):
-    root = etree.fromstring(
-        LOAD_POU_INSTANCES_PROJECT_TEMPLATE(body_type) % xml_string, 
-        PLCOpenParser)
-    return project_pou_instances_xpath[body_type](root)
+    root, error = LoadProjectXML(
+        LOAD_POU_INSTANCES_PROJECT_TEMPLATE(body_type) % xml_string)
+    return project_pou_instances_xpath[body_type](root), error
 
 def SaveProject(project, filepath):
     project_file = open(filepath, 'w')
--- a/plcopen/structures.py	Wed Sep 25 11:50:40 2013 +0200
+++ b/plcopen/structures.py	Fri Sep 27 16:22:40 2013 +0200
@@ -42,8 +42,10 @@
 
 ScriptDirectory = os.path.split(os.path.realpath(__file__))[0]
 
-StdBlockLibrary = LoadProject(os.path.join(ScriptDirectory, "Standard_Function_Blocks.xml"))
-AddnlBlockLibrary = LoadProject(os.path.join(ScriptDirectory, "Additional_Function_Blocks.xml"))
+StdBlockLibrary, error = LoadProject(
+    os.path.join(ScriptDirectory, "Standard_Function_Blocks.xml"))
+AddnlBlockLibrary, error = LoadProject(
+    os.path.join(ScriptDirectory, "Additional_Function_Blocks.xml"))
 
 StdBlockComments = {
     "SR": _("SR bistable\nThe SR bistable is a latch where the Set dominates."),
--- a/py_ext/PythonFileCTNMixin.py	Wed Sep 25 11:50:40 2013 +0200
+++ b/py_ext/PythonFileCTNMixin.py	Fri Sep 27 16:22:40 2013 +0200
@@ -37,12 +37,20 @@
                 (re.compile("(?<!<xhtml:p>)(?:<!\[CDATA\[)"), "<xhtml:p><![CDATA["),
                 (re.compile("(?:]]>)(?!</xhtml:p>)"), "]]></xhtml:p>")]:
                 pythonfile_xml = cre.sub(repl, pythonfile_xml)
-            python_code = etree.fromstring(pythonfile_xml, PythonParser)
-            
-            self.CodeFile.globals.setanyText(python_code.getanyText())
-            os.remove(filepath)
-            self.CreateCodeFileBuffer(False)
-            self.OnCTNSave()
+            
+            try:
+                python_code, error = PythonParser.LoadXMLString(pythonfile_xml)
+                if error is None:    
+                    self.CodeFile.globals.setanyText(python_code.getanyText())
+                    os.remove(filepath)
+                    self.CreateCodeFileBuffer(False)
+                    self.OnCTNSave()
+            except Exception, exc:
+                error = unicode(exc)
+            
+            if error is not None:
+                self.GetCTRoot().logger.write_error(
+                    _("Couldn't import old %s file.") % CTNName)
     
     def CodeFileName(self):
         return os.path.join(self.CTNPath(), "pyfile.xml")
--- a/xmlclass/xmlclass.py	Wed Sep 25 11:50:40 2013 +0200
+++ b/xmlclass/xmlclass.py	Fri Sep 27 16:22:40 2013 +0200
@@ -1730,7 +1730,7 @@
 
 class XMLClassParser(etree.XMLParser):
 
-    def __init__(self, namespaces, default_namespace_format, base_class, *args, **kwargs):
+    def __init__(self, namespaces, default_namespace_format, base_class, xsd_schema, *args, **kwargs):
         etree.XMLParser.__init__(self, *args, **kwargs)
         self.DefaultNamespaceFormat = default_namespace_format
         self.NSMAP = namespaces
@@ -1742,11 +1742,19 @@
         else:
             self.RootNSMAP = namespaces
         self.BaseClass = base_class
+        self.XSDSchema = xsd_schema
     
     def set_element_class_lookup(self, class_lookup):
         etree.XMLParser.set_element_class_lookup(self, class_lookup)
         self.ClassLookup = class_lookup
     
+    def LoadXMLString(self, xml_string):
+        tree = etree.fromstring(xml_string, self)
+        if not self.XSDSchema.validate(tree):
+            error = self.XSDSchema.error_log.last_error
+            return tree, (error.line, error.message)
+        return tree, None 
+    
     def Dumps(self, xml_obj):
         return etree.tostring(xml_obj)
     
@@ -1793,7 +1801,7 @@
         factory.NSMAP,
         factory.etreeNamespaceFormat,
         BaseClass[0] if len(BaseClass) == 1 else None,
-        schema = etree.XMLSchema(etree.fromstring(xsdstring)),
+        etree.XMLSchema(etree.fromstring(xsdstring)),
         strip_cdata = False, remove_blank_text=True)
     class_lookup = XMLElementClassLookUp(factory.ComputedClassesLookUp)
     parser.set_element_class_lookup(class_lookup)