diff --git a/pom.xml b/pom.xml index 60b8ceac..a7d8b930 100644 --- a/pom.xml +++ b/pom.xml @@ -401,6 +401,14 @@ 1.6.3 + + + com.ximpleware + vtd-xml + 2.13.4 + + + diff --git a/sparql-anything-xml/pom.xml b/sparql-anything-xml/pom.xml index 9db06ffc..43cfeffe 100644 --- a/sparql-anything-xml/pom.xml +++ b/sparql-anything-xml/pom.xml @@ -33,10 +33,24 @@ sparql-anything-model ${project.version} + + + + com.ximpleware + vtd-xml + + junit junit - 4.13.1 + test + + + + + com.github.sparqlanything + sparql-anything-testutils + ${project.version} test diff --git a/sparql-anything-xml/src/main/java/com/github/sparqlanything/xml/XMLTriplifier.java b/sparql-anything-xml/src/main/java/com/github/sparqlanything/xml/XMLTriplifier.java index fa369692..52c4632c 100644 --- a/sparql-anything-xml/src/main/java/com/github/sparqlanything/xml/XMLTriplifier.java +++ b/sparql-anything-xml/src/main/java/com/github/sparqlanything/xml/XMLTriplifier.java @@ -22,8 +22,20 @@ package com.github.sparqlanything.xml; import com.github.sparqlanything.model.FacadeXGraphBuilder; +import com.github.sparqlanything.model.Slice; +import com.github.sparqlanything.model.Slicer; import com.github.sparqlanything.model.Triplifier; import com.github.sparqlanything.model.TriplifierHTTPException; +import com.ximpleware.AutoPilot; +import com.ximpleware.EncodingException; +import com.ximpleware.NavException; +import com.ximpleware.ParseException; +import com.ximpleware.VTDGen; +import com.ximpleware.VTDNav; +import com.ximpleware.XPathEvalException; +import com.ximpleware.XPathParseException; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.jena.ext.com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,20 +56,136 @@ import java.util.Deque; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; -public class XMLTriplifier implements Triplifier { +public class XMLTriplifier implements Triplifier, Slicer { private static final Logger log = LoggerFactory.getLogger(XMLTriplifier.class); - @Override - public void triplify(Properties properties, FacadeXGraphBuilder builder) throws IOException, TriplifierHTTPException { -// URL url = Triplifier.getLocation(properties); -// -// if (url == null) -// return; + public void transformWithXPath(List xpaths, Properties properties, FacadeXGraphBuilder builder) throws IOException, TriplifierHTTPException { + + String namespace = Triplifier.getNamespaceArgument(properties); + String dataSourceId = Triplifier.getRootArgument(properties); + String root = Triplifier.getRootArgument(properties); + + builder.addRoot(dataSourceId, root); + try { + VTDGen vg = new VTDGen(); + byte[] bytes = IOUtils.toByteArray(Triplifier.getInputStream(properties)); + vg.setDoc(bytes); + vg.parse(false); + VTDNav vn = vg.getNav(); + Iterator xit = xpaths.iterator(); + while(xit.hasNext()) { + String xpath = xit.next(); + log.debug("Evaluating XPath: {}", xpath); + // vg.parse(false); // set namespace awareness to true + AutoPilot ap = new AutoPilot(vn); + //ap.declareXPathNameSpace("ns1","http://purl.org/dc/elements/1.1/"); + ap.selectXPath(xpath); + int result = -1; + int count = 1; + while ((result = ap.evalXPath()) != -1) { + transformFromXPath(vn, result, count, root, dataSourceId, properties, builder); + count++; + } + log.debug("XPath: {} matches", count); + } + + } catch (XPathEvalException | NavException | ParseException | XPathParseException e){ + log.error("Error while evaluating XPath expression"); + throw new IOException(e); + } + } + + public int transformFromXPath(VTDNav vn, int result, int child, String parentId, String dataSourceId, Properties properties, FacadeXGraphBuilder builder) throws NavException { + log.trace(" -- index: {} type: {}", result, vn.getTokenType(result)); + switch (vn.getTokenType(result)) { + case VTDNav.TOKEN_STARTING_TAG: + String tag = vn.toString(result); + log.trace(" -- tag: {} ", tag); + String childId = String.join("", parentId , "/" , Integer.toString(child), ":", tag); + builder.addContainer(dataSourceId, parentId, child, childId); + + // Attributes + int attrCount = vn.getAttrCount(); + log.trace(" -- attr count: {}", attrCount); + int increment = 0; + if (attrCount > 0) { + for (int i = result + 1; i <= result + attrCount; i += 2) { + // Not sure why but sometime attrCount is not reliable + if(vn.getTokenType(i) != VTDNav.TOKEN_ATTR_NAME){ + break; + } + String key = vn.toString(i); + String value = vn.toString(i + 1); + log.trace(" -- attr: {} = {}", key, value); + builder.addValue(dataSourceId, childId, key, value); + increment += 2; + } + } + // Get the text + int t = vn.getText(); // get the index of the text (char data or CDATA) + if (t != -1) { + String text = vn.toNormalizedString(t); + log.trace(" -- text: {}", text); + builder.addValue(dataSourceId, childId, 1, text); + } + + // Iterate on Children until complete + int tokenDepth = vn.getTokenDepth(result); + int index = result + increment; + int childc = 1; + while(true){ + index++; + int type = vn.getTokenType(index); + String s = vn.toString(index); + int d = vn.getTokenDepth(index); + // If type is element and depth is not greater than tokenDepth, break! + if((type == VTDNav.TOKEN_STARTING_TAG && d <=tokenDepth) || (type == VTDNav.TOKEN_STARTING_TAG && s.equals(""))){ + break; + } + log.trace( " ... index: {} depth: {} type: {} string: {}", index, d, type, s); + index = transformFromXPath(vn, index, childc, childId, dataSourceId, properties, builder); + childc++; + } + return index - 1; + case VTDNav.TOKEN_ATTR_NAME: + // Attribute + String name = vn.toString(result); + String value = vn.toString(result + 1); + log.trace("Attribute {} = {}", name, value); + String attrChildId = String.join("", parentId , "/" , Integer.toString(child), ":", name); + builder.addContainer(dataSourceId, parentId, child, attrChildId); + builder.addValue(dataSourceId, attrChildId, name, value); + return result + 1; + case VTDNav.TOKEN_ATTR_VAL: + // Attribute value + log.trace("Attribute value: {}", vn.toString(result)); + builder.addValue(dataSourceId, parentId, child, vn.toString(result)); + break; + case VTDNav.TOKEN_CHARACTER_DATA: + // Text + String text = vn.toNormalizedString(result); + log.trace("Text: {}", text); + builder.addValue(dataSourceId, parentId, child, vn.toString(result)); + break; + case VTDNav.TOKEN_DEC_ATTR_NAME: + log.trace("Attribute (dec): {} = {}", vn.toString(result), vn.toString(result + 1)); + return result + 1; + case VTDNav.TOKEN_DEC_ATTR_VAL: + log.trace("Attribute value (dec) ", vn.toString(result)); + break; + default: + log.warn("Ignored event: {} {}", vn.getTokenType(result), vn.toString(result)); + } + return result; + } + + public void transformSAX(Properties properties, FacadeXGraphBuilder builder) throws IOException, TriplifierHTTPException { String namespace = Triplifier.getNamespaceArgument(properties); String dataSourceId = Triplifier.getRootArgument(properties); @@ -137,7 +265,7 @@ public void triplify(Properties properties, FacadeXGraphBuilder builder) throws log.trace("element open: {} [{}]", path, stack.size()); // XXX Create an RDF resource - String resourceId = path.substring(1); + String resourceId = StringUtils.join("", root, path); // If this is the root if (isRoot) { // Add type root @@ -203,4 +331,25 @@ public Set getMimeTypes() { public Set getExtensions() { return Sets.newHashSet("xml"); } + + + @Override + public void triplify(Properties properties, FacadeXGraphBuilder builder) throws IOException, TriplifierHTTPException { + List xpaths = Triplifier.getPropertyValues(properties, "xml.path"); + if(!xpaths.isEmpty()){ + transformWithXPath(xpaths, properties, builder); + }else{ + transformSAX(properties, builder); + } + } + + @Override + public Iterable slice(Properties p) throws IOException, TriplifierHTTPException { + return null; + } + + @Override + public void triplify(Slice slice, Properties p, FacadeXGraphBuilder builder) { + + } } diff --git a/sparql-anything-xml/src/test/java/com/github/sparqlanything/xml/MoreXMLTriplifierTest.java b/sparql-anything-xml/src/test/java/com/github/sparqlanything/xml/MoreXMLTriplifierTest.java new file mode 100644 index 00000000..e0ea63e8 --- /dev/null +++ b/sparql-anything-xml/src/test/java/com/github/sparqlanything/xml/MoreXMLTriplifierTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2022 SPARQL Anything Contributors @ http://github.com/sparql-anything + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.github.sparqlanything.xml; + +import com.github.sparqlanything.testutils.AbstractTriplifierTester; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFDataMgr; +import org.junit.Test; + +import java.util.Properties; + +public class MoreXMLTriplifierTest extends AbstractTriplifierTester { + + public MoreXMLTriplifierTest() { + super(new XMLTriplifier(), new Properties(), "xml", "ttl"); + } + + @Override + protected void properties(Properties properties) { + if(name.getMethodName().equals("testSimple$1")){ + properties.put("blank-nodes", "false"); + }else + if(name.getMethodName().equals("testBooks$1")){ + properties.put("blank-nodes", "false"); + }else + if(name.getMethodName().equals("testBooks_1$1")){ + properties.put("blank-nodes", "false"); + properties.put("xml.path", "//book"); + } + } + + @Test + public void testSimple$1(){ + //RDFDataMgr.write(System.err, result, Lang.TTL); + assertResultIsIsomorphicWithExpected(); + } + + @Test + public void testBooks$1(){ +// RDFDataMgr.write(System.err, result, Lang.TTL); + assertResultIsIsomorphicWithExpected(); + } + + @Test + public void testBooks_1$1(){ + RDFDataMgr.write(System.err, result, Lang.TTL); + assertResultIsIsomorphicWithExpected(); + } +} diff --git a/sparql-anything-xml/src/test/java/com/github/sparqlanything/xml/XMLTriplifierTest.java b/sparql-anything-xml/src/test/java/com/github/sparqlanything/xml/XMLTriplifierTest.java index 9c7caa58..fd400d9f 100644 --- a/sparql-anything-xml/src/test/java/com/github/sparqlanything/xml/XMLTriplifierTest.java +++ b/sparql-anything-xml/src/test/java/com/github/sparqlanything/xml/XMLTriplifierTest.java @@ -36,7 +36,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; import java.io.IOException; +import java.io.InputStream; import java.net.URL; import java.util.Iterator; import java.util.Properties; diff --git a/sparql-anything-xml/src/test/java/com/github/sparqlanything/xml/XPathSandbox.java b/sparql-anything-xml/src/test/java/com/github/sparqlanything/xml/XPathSandbox.java new file mode 100644 index 00000000..18467979 --- /dev/null +++ b/sparql-anything-xml/src/test/java/com/github/sparqlanything/xml/XPathSandbox.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2022 SPARQL Anything Contributors @ http://github.com/sparql-anything + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.github.sparqlanything.xml; + +import com.ximpleware.AutoPilot; +import com.ximpleware.VTDGen; +import com.ximpleware.VTDNav; +import org.apache.commons.io.IOUtils; +import org.junit.Ignore; +import org.junit.Test; + +public class XPathSandbox { + + @Ignore + @Test + public void test() throws Exception { + printExprValue("/books/*"); + printExprValue("//book"); + printExprValue("//book[@isbn='978-3-12-148410-0']"); + printExprValue("//book/@isbn"); + printExprValue("//title/text()"); + } + + @Ignore + @Test + public void test2() throws Exception { + traverseMatches("//title"); +// printExprValue("/books/*"); +// printExprValue("//book"); +// printExprValue("//book[@isbn='978-3-12-148410-0']"); +// printExprValue("//book/@isbn"); +// printExprValue("//title/text()"); + } + + public void traverseMatches(String xpath) throws Exception{ + System.out.println("Expression: " + xpath); + VTDGen vg = new VTDGen(); + byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream("./test-xpaths.xml")); + vg.setDoc(bytes); + vg.parse(false); + VTDNav vn = vg.getNav(); +// vg.parse(false); // set namespace awareness to true + AutoPilot ap = new AutoPilot(vn); + //ap.declareXPathNameSpace("ns1","http://purl.org/dc/elements/1.1/"); + ap.selectXPath(xpath); + int result = -1; + int count = 0; + while ((result = ap.evalXPath()) != -1) { + traverse(vn, result); + System.out.println(" *** "); + count++; + } + System.out.println("Total # of element " + count); + System.out.println("----------------------------"); + } + + private void printExprValue(String xpath) throws Exception{ + System.out.println("Expression: " + xpath); + VTDGen vg = new VTDGen(); + byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream("./test-xpaths.xml")); + vg.setDoc(bytes); + vg.parse(false); + VTDNav vn = vg.getNav(); +// vg.parse(false); // set namespace awareness to true + AutoPilot ap = new AutoPilot(vn); + //ap.declareXPathNameSpace("ns1","http://purl.org/dc/elements/1.1/"); + ap.selectXPath(xpath); + int result = -1; + int count = 0; + while ((result = ap.evalXPath()) != -1) { + System.out.print("" + result + "[" + vn.getTokenType(result) + "] "); + switch(vn.getTokenType(result)){ + case VTDNav.TOKEN_STARTING_TAG: + System.out.println("Tag "+vn.toString(result)); + int nesting = vn.getNestingLevel(); + // Attributes + int attrCount = vn.getAttrCount(); + if (attrCount > 0) { + for (int i = result + 1; i <= result + attrCount; i += 2) { + System.out.println(" - Attr " + vn.toString(i) + " = " + vn.toString(i + 1)); + } + } + // Print text + int t = vn.getText(); // get the index of the text (char data or CDATA) + if (t != -1) + System.out.println(" Text ==> " + vn.toNormalizedString(t)); + break; + case VTDNav.TOKEN_ATTR_NAME: + // Attribute + System.out.println("Attribute " + vn.toString(result) ); + System.out.println( " = " + vn.toString(result + 1)); + break; + case VTDNav.TOKEN_ATTR_VAL: + // Attribute value + System.out.println("Attribute value" + vn.toString(result) ); +// System.out.println( " = " + vn.toString(result + 1)); + break; + case VTDNav.TOKEN_CHARACTER_DATA: + // Text + System.out.println("Text"); + System.out.println(" " + vn.toNormalizedString(result)); + break; + case VTDNav.TOKEN_DEC_ATTR_NAME: + System.out.println("Attribute (dec) " + vn.toString(result) ); + System.out.println( " = " + vn.toString(result + 1)); + break; + case VTDNav.TOKEN_DEC_ATTR_VAL: + System.out.println("Attribute value (dec) " + vn.toString(result) ); + break; + default: + System.out.println("????"); + System.out.println(" ==> " + vn.toString(result)); + + } + count++; + } + System.out.println("Total # of element " + count); + System.out.println("----------------------------"); + } + + public void traverse(VTDNav nav, int index) throws Exception{ + int tokenDepth = nav.getTokenDepth(index); + String tokenName = nav.toString(index); + // Leave out attributes + // Find child elements + while(true){ + index++; +// nav.toElement(index); + int type = nav.getTokenType(index); + String s = nav.toString(index); + int d = nav.getTokenDepth(index); + // If type is element and depth is not greater than tokenDepth, break! + if((type == VTDNav.TOKEN_STARTING_TAG && d <=tokenDepth) || (type == VTDNav.TOKEN_STARTING_TAG && s.equals(""))){ + break; + } + System.out.println( " ... index: " + index + " depth: " + d + " type: " + type + " string: " + s); + + } + } +} diff --git a/sparql-anything-xml/src/test/resources/Books.ttl b/sparql-anything-xml/src/test/resources/Books.ttl new file mode 100644 index 00000000..560cb6c5 --- /dev/null +++ b/sparql-anything-xml/src/test/resources/Books.ttl @@ -0,0 +1,92 @@ +@prefix rdf: . +@prefix rdfs: . +@prefix fx: . +@prefix xyz: . +@prefix xsd: . + + + a xyz:price ; + rdf:_1 "5.95" . + + a xyz:title ; + rdf:_1 "Midnight Rain" . + + a xyz:price ; + rdf:_1 "44.95" . + + a xyz:book ; + rdf:_1 ; + rdf:_2 ; + rdf:_3 ; + rdf:_4 ; + rdf:_5 ; + rdf:_6 ; + xyz:isbn "978-3-12-148410-0" . + + a xyz:title ; + rdf:_1 "XML Developer's Guide" . + + a xyz:books , ; + rdf:_1 ; + rdf:_2 ; + rdf:_3 . + + a xyz:author ; + rdf:_1 "Ralls, Kim" . + + a xyz:book ; + rdf:_1 ; + rdf:_2 ; + rdf:_3 ; + rdf:_4 ; + rdf:_5 ; + rdf:_6 ; + xyz:isbn "928-3-16-148410-0" . + + a xyz:description ; + rdf:_1 "A former architect battles corporate zombies,\n an evil sorceress, and her own childhood to become queenof the world." . + + a xyz:genre ; + rdf:_1 "Fantasy" . + + a xyz:publish_date ; + rdf:_1 "2000-10-01" . + + a xyz:author ; + rdf:_1 "Gambardella, Matthew" . + + a xyz:description ; + rdf:_1 "An in-depth look at creating applications\n with XML." . + + a xyz:genre ; + rdf:_1 "Fantasy" . + + a xyz:book ; + rdf:_1 ; + rdf:_2 ; + rdf:_3 ; + rdf:_4 ; + rdf:_5 ; + rdf:_6 ; + xyz:isbn "432-3-16-143110-1" . + + a xyz:price ; + rdf:_1 "5.95" . + + a xyz:publish_date ; + rdf:_1 "2000-12-16" . + + a xyz:genre ; + rdf:_1 "Computer" . + + a xyz:author ; + rdf:_1 "Corets, Eva" . + + a xyz:publish_date ; + rdf:_1 "2000-11-17" . + + a xyz:title ; + rdf:_1 "Maeve Ascendant" . + + a xyz:description ; + rdf:_1 "After the collapse of a nanotechnology\n society in England, the young survivors lay thefoundation for a new society." . \ No newline at end of file diff --git a/sparql-anything-xml/src/test/resources/Books.xml b/sparql-anything-xml/src/test/resources/Books.xml new file mode 100644 index 00000000..2d48bc15 --- /dev/null +++ b/sparql-anything-xml/src/test/resources/Books.xml @@ -0,0 +1,49 @@ + + + + + + Gambardella, Matthew + XML Developer's Guide + Computer + 44.95 + 2000-10-01 + An in-depth look at creating applications + with XML. + + + Ralls, Kim + Midnight Rain + Fantasy + 5.95 + 2000-12-16 + A former architect battles corporate zombies, + an evil sorceress, and her own childhood to become queen + of the world. + + + Corets, Eva + Maeve Ascendant + Fantasy + 5.95 + 2000-11-17 + After the collapse of a nanotechnology + society in England, the young survivors lay the + foundation for a new society. + + \ No newline at end of file diff --git a/sparql-anything-xml/src/test/resources/Books_1.ttl b/sparql-anything-xml/src/test/resources/Books_1.ttl new file mode 100644 index 00000000..38ec0654 --- /dev/null +++ b/sparql-anything-xml/src/test/resources/Books_1.ttl @@ -0,0 +1,135 @@ +@prefix rdf: . +@prefix rdfs: . +@prefix fx: . +@prefix xyz: . +@prefix xsd: . + + + + + "Fantasy" . + + + + "A former architect battles corporate zombies,\n an evil sorceress, and her own childhood to become queen\n of the world." . + + + + "Gambardella, Matthew" . + + + a ; + + ; + + ; + + . + + + + "Corets, Eva" . + + + + "2000-10-01" . + + + + "Fantasy" . + + + + "2000-12-16" . + + + + "5.95" . + + + + "Computer" . + + + + "Maeve Ascendant" . + + + + ; + + ; + + ; + + ; + + ; + + ; + + "978-3-12-148410-0" . + + + + "5.95" . + + + + "An in-depth look at creating applications\n with XML." . + + + + "2000-11-17" . + + + + "Ralls, Kim" . + + + + "Midnight Rain" . + + + + "44.95" . + + + + "XML Developer's Guide" . + + + + "After the collapse of a nanotechnology\n society in England, the young survivors lay the\n foundation for a new society." . + + + + ; + + ; + + ; + + ; + + ; + + ; + + "928-3-16-148410-0" . + + + + ; + + ; + + ; + + ; + + ; + + ; + + "432-3-16-143110-1" . \ No newline at end of file diff --git a/sparql-anything-xml/src/test/resources/Books_1.xml b/sparql-anything-xml/src/test/resources/Books_1.xml new file mode 100644 index 00000000..2d48bc15 --- /dev/null +++ b/sparql-anything-xml/src/test/resources/Books_1.xml @@ -0,0 +1,49 @@ + + + + + + Gambardella, Matthew + XML Developer's Guide + Computer + 44.95 + 2000-10-01 + An in-depth look at creating applications + with XML. + + + Ralls, Kim + Midnight Rain + Fantasy + 5.95 + 2000-12-16 + A former architect battles corporate zombies, + an evil sorceress, and her own childhood to become queen + of the world. + + + Corets, Eva + Maeve Ascendant + Fantasy + 5.95 + 2000-11-17 + After the collapse of a nanotechnology + society in England, the young survivors lay the + foundation for a new society. + + \ No newline at end of file diff --git a/sparql-anything-xml/src/test/resources/Simple.ttl b/sparql-anything-xml/src/test/resources/Simple.ttl new file mode 100644 index 00000000..2c020642 --- /dev/null +++ b/sparql-anything-xml/src/test/resources/Simple.ttl @@ -0,0 +1,17 @@ +@prefix rdf: . +@prefix rdfs: . +@prefix fx: . +@prefix xyz: . +@prefix xsd: . + + + a , ; + + . + + + a ; + + "Some text here" ; + + "food" . \ No newline at end of file diff --git a/sparql-anything-xml/src/test/resources/Simple.xml b/sparql-anything-xml/src/test/resources/Simple.xml new file mode 100644 index 00000000..460ae3ce --- /dev/null +++ b/sparql-anything-xml/src/test/resources/Simple.xml @@ -0,0 +1,3 @@ + + Some text here + \ No newline at end of file diff --git a/sparql-anything-xml/src/test/resources/test-xpaths.xml b/sparql-anything-xml/src/test/resources/test-xpaths.xml new file mode 100644 index 00000000..2d48bc15 --- /dev/null +++ b/sparql-anything-xml/src/test/resources/test-xpaths.xml @@ -0,0 +1,49 @@ + + + + + + Gambardella, Matthew + XML Developer's Guide + Computer + 44.95 + 2000-10-01 + An in-depth look at creating applications + with XML. + + + Ralls, Kim + Midnight Rain + Fantasy + 5.95 + 2000-12-16 + A former architect battles corporate zombies, + an evil sorceress, and her own childhood to become queen + of the world. + + + Corets, Eva + Maeve Ascendant + Fantasy + 5.95 + 2000-11-17 + After the collapse of a nanotechnology + society in England, the young survivors lay the + foundation for a new society. + + \ No newline at end of file