From 38972037926720692aab1440a5f477306481e481 Mon Sep 17 00:00:00 2001 From: Victor Lowther Date: Tue, 30 Dec 2014 16:06:24 -0600 Subject: [PATCH] Add experimental namespace support. Based on similar support from github.com/masterzen/xmlpath --- all_test.go | 74 ++++++++++++++++++++++++++++++++ doc.go | 19 ++++++++- path.go | 118 ++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 173 insertions(+), 38 deletions(-) diff --git a/all_test.go b/all_test.go index dd8ae18..e6f7cf0 100644 --- a/all_test.go +++ b/all_test.go @@ -322,6 +322,80 @@ var libraryXml = []byte(` `) +func (s *BasicSuite) TestNamespace(c *C) { + node, err := xmlpath.Parse(bytes.NewBuffer(namespaceXml)) + c.Assert(err, IsNil) + for _, test := range namespaceTable { + cmt := Commentf("xml path: %s", test.path) + path, err := xmlpath.CompileWithNamespace(test.path, namespaces) + if want, ok := test.result.(cerror); ok { + c.Assert(err, ErrorMatches, string(want), cmt) + c.Assert(path, IsNil, cmt) + continue + } + c.Assert(err, IsNil) + switch want := test.result.(type) { + case string: + got, ok := path.String(node) + c.Assert(ok, Equals, true, cmt) + c.Assert(got, Equals, want, cmt) + c.Assert(path.Exists(node), Equals, true, cmt) + iter := path.Iter(node) + iter.Next() + node := iter.Node() + c.Assert(node.String(), Equals, want, cmt) + c.Assert(string(node.Bytes()), Equals, want, cmt) + case []string: + var alls []string + var allb []string + iter := path.Iter(node) + for iter.Next() { + alls = append(alls, iter.Node().String()) + allb = append(allb, string(iter.Node().Bytes())) + } + c.Assert(alls, DeepEquals, want, cmt) + c.Assert(allb, DeepEquals, want, cmt) + s, sok := path.String(node) + b, bok := path.Bytes(node) + if len(want) == 0 { + c.Assert(sok, Equals, false, cmt) + c.Assert(bok, Equals, false, cmt) + c.Assert(s, Equals, "") + c.Assert(b, IsNil) + } else { + c.Assert(sok, Equals, true, cmt) + c.Assert(bok, Equals, true, cmt) + c.Assert(s, Equals, alls[0], cmt) + c.Assert(string(b), Equals, alls[0], cmt) + c.Assert(path.Exists(node), Equals, true, cmt) + } + case exists: + wantb := bool(want) + ok := path.Exists(node) + c.Assert(ok, Equals, wantb, cmt) + _, ok = path.String(node) + c.Assert(ok, Equals, wantb, cmt) + } + } +} + +var namespaceXml = []byte(`http://schemas.microsoft.com/wbem/wsman/1/windows/shell/ReceiveResponseuuid:AAD46BD4-6315-4C3C-93D4-94A55773287Dhttp://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousuuid:18A52A06-9027-41DC-8850-3F244595AF62VGhhdCdzIGFsbCBmb2xrcyEhIQ==VGhpcyBpcyBzdGRlcnIsIEknbSBwcmV0dHkgc3VyZSE=`) + +var namespaces = []xmlpath.Namespace { + { "a", "http://schemas.xmlsoap.org/ws/2004/08/addressing" }, + { "rsp", "http://schemas.microsoft.com/wbem/wsman/1/windows/shell" }, +} + +var namespaceTable = []struct{ path string; result interface{} }{ + { "//a:To", "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous" }, + { "//rsp:Stream[@Name='stdout']", "VGhhdCdzIGFsbCBmb2xrcyEhIQ==" }, + { "//rsp:CommandState/@CommandId", "1A6DEE6B-EC68-4DD6-87E9-030C0048ECC4" }, + { "//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']", exists(false) }, + { "//rsp:Stream", []string{ "VGhhdCdzIGFsbCBmb2xrcyEhIQ==", "VGhpcyBpcyBzdGRlcnIsIEknbSBwcmV0dHkgc3VyZSE=" }}, + { "//s:Header", cerror(`.*: unknown namespace prefix: s`) }, +} + + func (s *BasicSuite) BenchmarkParse(c *C) { for i := 0; i < c.N; i++ { _, err := xmlpath.Parse(bytes.NewBuffer(instancesXml)) diff --git a/doc.go b/doc.go index e81d737..3f67965 100644 --- a/doc.go +++ b/doc.go @@ -17,7 +17,8 @@ // - All node types except for namespace are supported // - Predicates may be [N], [path], [not(path)], [path=literal] or [contains(path, literal)] // - Predicates may be joined with "or", "and", and parenthesis -// - Richer expressions and namespaces are not supported +// - Namespaces are experimentally supported +// - Richer expressions are not supported // // For example, assuming the following document: // @@ -72,4 +73,20 @@ // fmt.Println("Found:", value) // } // +// To use xmlpath with namespaces, it is required to give the supported set of namespace +// when compiling: +// +// var namespaces = []xmlpath.Namespace { +// { "s", "http://www.w3.org/2003/05/soap-envelope" }, +// { "a", "http://schemas.xmlsoap.org/ws/2004/08/addressing" }, +// } +// path := xmlpath.MustCompileWithNamespace("/s:Header/a:To", namespaces) +// root, err := xmlpath.Parse(file) +// if err != nil { +// log.Fatal(err) +// } +// if value, ok := path.String(root); ok { +// fmt.Println("Found:", value) +// } +// package xmlpath diff --git a/path.go b/path.go index db38ed5..c53cc73 100644 --- a/path.go +++ b/path.go @@ -6,6 +6,12 @@ import ( "unicode/utf8" ) +// Namespace represents a given XML Namespace +type Namespace struct { + Prefix string + Uri string +} + // Path is a compiled path that can be applied to a context // node to obtain a matching node set. // A single Path can be applied concurrently to any number @@ -407,17 +413,42 @@ func (andPredicate) predicate() {} func (orPredicate) predicate() {} type pathStep struct { - root bool - axis string - name string - kind nodeKind - pred predicate + root bool + axis string + name string + prefix string + uri string + kind nodeKind + pred predicate } func (step *pathStep) match(node *Node) bool { return node.kind != endNode && (step.kind == anyNode || step.kind == node.kind) && - (step.name == "*" || node.name.Local == step.name) + (step.name == "*" || node.name.Local == step.name && + (node.name.Space != "" && node.name.Space == step.uri || node.name.Space == "")) +} + +func compile(path string, ns []Namespace) (*Path, error) { + c := pathCompiler{path, 0, ns} + if path == "" { + return nil, c.errorf("empty path") + } + p, err := c.parsePath() + if err != nil { + return nil, err + } + return p, nil +} + +// Compile returns the compiled path. +func Compile(path string) (*Path, error) { + return compile(path, []Namespace{}) +} + +// Compile the path with the knowledge of the given namespaces +func CompileWithNamespace(path string, ns []Namespace) (*Path, error) { + return compile(path, ns) } // MustCompile returns the compiled path, and panics if @@ -430,22 +461,21 @@ func MustCompile(path string) *Path { return e } -// Compile returns the compiled path. -func Compile(path string) (*Path, error) { - c := pathCompiler{path, 0} - if path == "" { - return nil, c.errorf("empty path") - } - p, err := c.parsePath() +// MustCompileWithNamespace returns the compiled path with +// knowledge of the given namespaces, and panics if +// there are any errors. +func MustCompileWithNamespace(path string, ns []Namespace) *Path { + e, err := CompileWithNamespace(path, ns) if err != nil { - return nil, err + panic(err) } - return p, nil + return e } type pathCompiler struct { path string i int + ns []Namespace } func (c *pathCompiler) errorf(format string, args ...interface{}) error { @@ -496,29 +526,43 @@ func (c *pathCompiler) parsePath() (path *Path, err error) { } else { if c.skipByte(':') { if !c.skipByte(':') { - return nil, c.errorf("missing ':'") - } - c.skipSpaces() - switch step.name { - case "attribute": - step.kind = attrNode - case "self", "child", "parent": - case "descendant", "descendant-or-self": - case "ancestor", "ancestor-or-self": - case "following", "following-sibling": - case "preceding", "preceding-sibling": - default: - return nil, c.errorf("unsupported axis: %q", step.name) - } - step.axis = step.name - - mark = c.i - if !c.skipName() { - return nil, c.errorf("missing name") + mark = c.i + if c.skipName() { + step.prefix = step.name + step.name = c.path[mark:c.i] + found := false + for _,ns := range c.ns { + if ns.Prefix == step.prefix { + step.uri = ns.Uri + found = true + break + } + } + if !found { + return nil,c.errorf("unknown namespace prefix: %s",step.prefix) + } + } + } else { + c.skipSpaces() + switch step.name { + case "attribute": + step.kind = attrNode + case "self", "child", "parent": + case "descendant", "descendant-or-self": + case "ancestor", "ancestor-or-self": + case "following", "following-sibling": + case "preceding", "preceding-sibling": + default: + return nil, c.errorf("unsupported axis: %q", step.name) + } + step.axis = step.name + mark = c.i + if !c.skipName() { + return nil, c.errorf("missing name") + } + step.name = c.path[mark:c.i] + c.skipSpaces() } - step.name = c.path[mark:c.i] - - c.skipSpaces() } if c.skipByte('(') { c.skipSpaces()