Java-XML-和-JSON-教程-二-

72 阅读20分钟

Java XML 和 JSON 教程(二)

原文:Java XML and JSON

协议:CC BY-NC-SA 4.0

五、使用 XPath 选择节点

Java 包含一个 XPath API,用于简化对 DOM 树节点的访问。本章向您介绍 XPath。

什么是 XPath?

XPath 是一种非 XML 声明性查询语言(由 W3C 定义),用于选择 XML 文档的信息集项目作为一个或多个节点。例如,您可以使用 XPath 来定位清单 1-1 的第三个ingredient元素并返回这个元素节点。

除了简化对 DOM 树节点的访问之外,XPath 通常用于 XSLT 的上下文中(在第六章中讨论),通常用来选择(通过 XPath 表达式)那些要复制到输出文档的输入文档元素。Java 8 支持 XPath 1.0,被分配了包javax.xml.xpath

XPath 语言入门

XPath 将 XML 文档视为从根节点开始的节点树。这种语言识别七种节点:元素、属性、文本、名称空间、处理指令、注释和文档。它不识别 CDATA 节、实体引用或文档类型声明。

Note

DOM 树的根节点(一个org.w3c.dom.Document对象)与文档的根元素不同。DOM 树的根节点包含整个文档,包括根元素、出现在根元素开始标记之前的任何注释或处理指令,以及出现在根元素结束标记之后的任何注释或处理指令。

位置路径表达式

XPath 为选择节点提供了位置路径表达式。位置路径表达式通过从上下文节点(根节点或当前节点的其他文档节点)开始的一系列步骤来定位节点。返回的节点集(称为节点集)可能为空,也可能包含一个或多个节点。

最简单的位置路径表达式选择文档的根节点,由一个正斜杠字符(/)组成。下一个最简单的位置路径表达式是元素的名称,它选择具有该名称的上下文节点的所有子元素。例如,ingredient引用清单 1-1 的菜谱文档中上下文节点的所有ingredient子元素。当上下文节点是ingredients时,这个 XPath 表达式返回一组三个ingredient节点。但是,如果recipeinstructions恰好是上下文节点,ingredient不会返回任何节点(ingredient只是ingredients的子节点)。当表达式以正斜杠(/)开始时,表达式表示从根节点开始的绝对路径。例如,表达式/movie选择清单 1-2 的电影文档中根节点的所有movie子元素。

属性也由位置路径表达式处理。要选择元素的属性,请指定@,后跟属性的名称。例如,@qty选择上下文节点的qty属性节点。

在大多数情况下,您将使用根节点、元素节点和属性节点。但是,您可能还需要使用名称空间节点、文本节点、处理指令节点和注释节点。与通常由 XSLT 处理的名称空间节点不同,您更可能需要处理注释、文本和处理指令。XPath 提供了用于选择注释、文本和处理指令节点的comment()text()processing-instruction()函数。

comment()text()函数不需要参数,因为注释和文本节点没有名称。每个注释都是一个单独的注释节点,每个文本节点指定没有被标签打断的最长文本串。可以用一个标识处理指令目标的参数调用processing-instruction()函数。如果不带参数调用,则选择上下文节点的所有处理指令子节点。

XPath 为选择未知节点提供了三个通配符:

  • *匹配任何元素节点,不考虑节点的类型。它不匹配属性、文本节点、注释或处理指令节点。当您在*前放置一个名称空间前缀时,只有属于该名称空间的元素才被匹配。
  • node()是匹配所有节点的函数。
  • @*匹配所有属性节点。

Note

XPath 允许您使用竖线(|)进行多重选择。例如,author/*|publisher/*选择author的子节点和publisher的子节点,*|@*匹配所有元素和属性,但不匹配文本、注释或处理指令节点。

XPath 允许您通过使用/字符来分隔步骤,从而将它们组合成复合路径。对于以/开头的路径,第一个路径步长是相对于根节点的;否则,第一个路径步骤相对于另一个上下文节点。例如,/movie/name从根节点开始,选择根节点的所有movie元素子节点,选择所选movie节点的所有name子节点。如果您想要返回所选择的name元素的所有文本节点,您可以指定/movie/name/text()

复合路径可以包括//来从上下文节点的所有后代中选择节点(包括上下文节点)。当放置在表达式的开始时,//从整个树中选择节点。例如,//ingredient选择树中的所有ingredient节点。

对于允许您用单句点(.)标识当前目录,用双句点(..)标识其父目录的文件系统,您可以指定单句点来表示当前节点,并用双句点来表示当前节点的父节点。(通常在 XSLT 中使用一个句点来表示您想要访问当前匹配元素的值。)

可能有必要缩小 XPath 表达式返回的节点的选择范围。例如,表达式/recipe/ingredients/ingredient返回所有的ingredient节点,但是也许您只想返回第一个ingredient节点。您可以通过在位置路径中包含谓词来缩小选择范围。

谓词是一个方括号分隔的布尔表达式,针对每个选定的节点进行测试。如果表达式计算结果为true,则该节点包含在 XPath 表达式返回的节点集中;否则,该节点不会包含在集合中。例如,/recipe/ingredients/ingredient[1]选择第一个ingredient元素,它是ingredients元素的子元素。

谓词可以包括预定义的函数(如last()position())、运算符(如-<=)以及其他项。请考虑以下示例:

  • /recipe/ingredients/ingredient[last()]选择最后一个ingredient元素,它是ingredients元素的子元素。
  • /recipe/ingredients/ingredient[last() - 1]选择倒数第二个ingredient元素,它是ingredients元素的子元素。
  • /recipe/ingredients/ingredient[position() < 3]选择前两个ingredient元素,它们是ingredients元素的子元素。
  • //ingredient[@qty]选择所有具有qty属性的ingredient元素(无论它们位于何处)。
  • //ingredient[@qty='1']//ingredient[@qty="1"]选择所有具有qty属性值1ingredient元素(无论它们位于何处)。

Note

XPath 预定义了几个用于节点集的函数:last()返回一个标识最后一个节点的数字,position()返回一个标识节点位置的数字,count()返回其节点集参数中的节点数,id()通过元素的唯一 id 选择元素并返回这些元素的节点集,local-name()返回其节点集参数中第一个节点的限定名的本地部分,namespace-uri()返回其节点集参数中第一个节点的限定名的名称空间部分,name()返回其节点集中第一个节点的限定名

尽管谓词应该是布尔表达式,但谓词可能不会计算为布尔值。例如,它可以计算数字或字符串—XPath 支持布尔值、数字(IEEE 754 双精度浮点值)、字符串表达式类型以及位置路径表达式的节点集类型。如果谓词的计算结果是一个数字,当它等于上下文节点的位置时,XPath 会将这个数字转换为true;否则,XPath 会将这个数字转换成false。如果谓词的结果是一个字符串,当字符串不为空时,XPath 会将该字符串转换为true;否则,XPath 会将该字符串转换为false。最后,如果一个谓词评估为一个节点集,当节点集非空时,XPath 将该节点集转换为true;否则,XPath 会将该节点集转换为false

Note

前面给出的位置路径表达式示例演示了 XPath 的缩写语法。但是,XPath 还支持完整的语法,该语法更好地描述了正在发生的事情,并且基于轴说明符,该说明符指示 XML 文档的树表示中的导航方向。例如,/movie/name使用缩写语法选择根节点的所有movie子元素,然后选择movie元素的所有name子元素,/child::movie/child::name使用扩展语法完成相同的任务。查看维基百科的“XPath”条目( http://en.wikipedia.org/wiki/XPath_1.0 )了解更多信息。

通用表达式

位置路径表达式(返回节点集)是 XPath 表达式的一种。XPath 还支持计算结果为布尔值(比如谓词)、数字或字符串类型的通用表达式;比如position() = 26.8"Hello"。XSLT 中经常使用通用表达式。

XPath 布尔值可以通过关系运算符<<=>>==!=进行比较。布尔表达式可以通过使用操作符andor来组合。此外,XPath 预定义了以下函数:

  • boolean()返回数字、字符串或节点集的布尔值。
  • 当其布尔参数为false时,not()返回true,反之亦然。
  • true()返回true
  • false()返回false
  • lang()根据上下文节点的语言(由xml:lang属性指定)是否与参数字符串指定的语言相同或者是该语言的子语言,返回truefalse

XPath 数值可以通过运算符+-*divmod(余数)进行操作;正斜杠不能用于除法,因为它用于分隔位置步骤。所有五个操作符的行为都像 Java 语言中的操作符一样。XPath 还预定义了以下函数:

  • number()将其参数转换为数字。
  • sum()返回其 nodeset 参数中节点所代表的数值之和。
  • floor()返回不大于其 number 参数的最大(最接近正无穷大)数,这是一个整数。
  • ceiling()返回不小于其 number 参数的最小(最接近负无穷大)数,这是一个整数。
  • round()返回与参数最接近的整数。当有两个这样的数字时,返回最接近正无穷大的一个。

XPath 字符串是用单引号或双引号括起来的有序字符序列。字符串文字不能包含同样用于分隔字符串的引号。例如,包含单引号的字符串不能用单引号分隔。XPath 提供了用于比较字符串的=!=操作符。XPath 还预定义了以下函数:

  • string()将其参数转换为字符串。
  • concat()返回其字符串参数的串联。
  • 当第一个参数字符串以第二个参数字符串开始时,starts-with()返回true(否则返回false)。
  • 当第一个参数字符串包含第二个参数字符串时,contains()返回true(否则返回false)。
  • substring-before()返回第一个参数字符串中第二个参数字符串第一次出现之前的第一个参数字符串的子字符串,或者当第一个参数字符串不包含第二个参数字符串时返回空字符串。
  • substring-after()返回第一个参数字符串中第二个参数字符串第一次出现后的第一个参数字符串的子字符串,或者当第一个参数字符串不包含第二个参数字符串时返回空字符串。
  • substring()返回第一个(字符串)参数的子字符串,从第二个(数字)参数指定的位置开始,长度由第三个(数字)参数指定。
  • string-length()返回其字符串参数中的字符数(或在没有参数的情况下转换为字符串时上下文节点的长度)。
  • normalize-space()返回带有空格的参数字符串,通过去除前导和尾随空格并用单个空格替换空格字符序列(或者在没有参数的情况下转换为字符串时在上下文节点上执行相同的操作)来规范化空格。
  • translate()返回第一个参数字符串,第二个参数字符串中出现的字符被第三个参数字符串中相应位置的字符替换。

XPath 和 DOM

假设你需要有人在你家买一袋糖。你会问这个人“请给我买些糖。”或者,你可以这样说:“请打开前门。走到人行道上。向左转。沿着人行道走三个街区。向右转。沿着人行道走一个街区。进入商店。去 7 号通道。沿着过道走两米。拿起一袋糖。走向收银台。付钱买糖。折回你的家。”大多数人会希望得到较短的指导,如果你养成了提供较长指导的习惯,他们可能会让你去某个机构。

遍历节点的 DOM 树类似于提供更长的指令序列。相比之下,XPath 让您通过简洁的指令遍历这棵树。要亲自了解这种差异,请考虑这样一个场景:您有一个基于 XML 的 contacts 文档,其中列出了您的各种专业联系人。清单 5-1 给出了这样一个文档的简单例子。

<?xml version="1.0"?>
<contacts>
   <contact>
      <name>John Doe</name>
      <city>Chicago</city>
      <city>Denver</city>
   </contact>
   <contact>
      <name>Jane Doe</name>
      <city>New York</city>
   </contact>
   <contact>
      <name>Sandra Smith</name>
      <city>Denver</city>
      <city>Miami</city>
   </contact>
   <contact>
     <name>Bob Jones</name>
     <city>Chicago</city>
   </contact>
</contacts>
Listing 5-1.XML-Based Contacts Database

清单 5-1 揭示了一个简单的 XML 语法,它由一个包含一系列contact元素的contacts根元素组成。每个contact元素包含一个name元素和一个或多个city元素(各种联系人经常出差,在每个城市花费大量时间)。为了保持示例简单,我没有提供 DTD 或模式。

假设您想查找并输出每年至少有一部分时间住在芝加哥的所有联系人的姓名。清单 5-2 将源代码呈现给一个用 DOM API 完成这项任务的DOMSearch应用程序。

import java.io.IOException;

import java.util.ArrayList;
import java.util.List; 

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.xml.sax.SAXException;

public class DOMSearch
{
   public static void main(String[] args)
   {
      try
      {
         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         DocumentBuilder db = dbf.newDocumentBuilder();
         Document doc = db.parse("contacts.xml");
         List<String> contactNames = new ArrayList<String>();
         NodeList contacts = doc.getElementsByTagName("contact");
         for (int i = 0; i < contacts.getLength(); i++)
         {
            Element contact = (Element) contacts.item(i);
            NodeList cities = contact.getElementsByTagName("city");
            boolean chicago = false;
            for (int j = 0; j < cities.getLength(); j++)
            {
               Element city = (Element) cities.item(j);
               NodeList children = city.getChildNodes();
               StringBuilder sb = new StringBuilder();
               for (int k = 0; k < children.getLength(); k++)
               {

                  Node child = children.item(k);
                  if (child.getNodeType() == Node.TEXT_NODE)
                     sb.append(child.getNodeValue());
               }
               if (sb.toString().equals("Chicago"))
               {
                  chicago = true;
                  break;
               }
            }
            if (chicago)
            {
               NodeList names = contact.getElementsByTagName("name");
               contactNames.add(names.item(0).getFirstChild().
                                getNodeValue());
            }
         }

         for (String contactName: contactNames)
            System.out.println(contactName);
      }
      catch (IOException ioe)
      {
         System.err.println("IOE: " + ioe);
      }
      catch (SAXException saxe)
      {
         System.err.println("SAXE: " + saxe);
      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (ParserConfigurationException pce)
      {
         System.err.println("PCE: " + pce);
      }
   }

}

Listing 5-2.Locating Chicago Contacts with the DOM API

在解析contacts.xml并构建 DOM 树之后,main()使用DocumentgetElementsByTagName()方法返回contact元素节点的org.w3c.dom.NodeList。对于这个列表的每个成员,main()提取contact元素节点,并使用这个节点和getElementsByTagName()返回一个contact元素节点的city元素节点的NodeList

对于cities列表中的每个成员,main()提取city元素节点,并使用该节点和getElementsByTagName()返回city元素节点的子节点的NodeList。本例中只有一个子文本节点,但是注释或处理指令的出现会增加子节点的数量。例如,<city>Chicago<!--The windy city--></city>将子节点的数量增加到 2。

如果子节点类型表明它是一个文本节点,则子节点的值(通过getNodeValue()获得)存储在字符串生成器中(在本例中,字符串生成器中只存储一个子节点。)如果构建器的内容表明已经找到了Chicago,则将chicago标志设置为true,并且执行离开cities循环。

如果在cities循环退出时设置了chicago标志,则调用当前contact元素节点的getElementsByTagName()方法来返回contact元素节点的name元素节点的NodeList(其中应该只有一个,我可以通过 DTD 或 schema 来实现)。现在很简单,从这个列表中提取第一个项目,调用这个项目上的getFirstChild()返回文本节点(我假设只有文本出现在<name></name>之间),调用文本节点上的getNodeValue()获得它的值,然后将它添加到contactNames列表中。

编译清单 5-2 如下:

javac DOMSearch.java

运行生成的应用程序,如下所示:

java DOMSearch

您应该观察到以下输出:

John Doe
Bob Jones

遍历 DOM 的节点树在最好的情况下是一项乏味的工作,在最坏的情况下容易出错。幸运的是,XPath 可以大大简化这种情况。

在编写清单 5-2 的 XPath 等价物之前,定义一个位置路径表达式是有帮助的。对于本例,该表达式是//contact[city = "Chicago"]/name/text(),它使用一个谓词选择包含一个Chicago city节点的所有contact节点,然后从这些contact节点中选择所有子name节点,最后从这些name节点中选择所有子文本节点。

清单 5-3 给出了一个XPathSearch应用程序的源代码,该应用程序使用这个 XPath 表达式和 Java 的 XPath API(由javax.xml.xpath包中的各种类型组成)来定位 Chicago 联系人。

import java.io.IOException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathException;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

import org.xml.sax.SAXException;

public class XPathSearch
{
   public static void main(String[] args)
   {

      try
      {
         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         DocumentBuilder db = dbf.newDocumentBuilder();
         Document doc = db.parse("contacts.xml");
         XPathFactory xpf = XPathFactory.newInstance();
         XPath xp = xpf.newXPath();
         XPathExpression xpe;
         xpe = xp.compile("//contact[city = 'Chicago']/name/text()");
         Object result = xpe.evaluate(doc, XPathConstants.NODESET);
         NodeList nl = (NodeList) result;
         for (int i = 0; i < nl.getLength(); i++)
            System.out.println(nl.item(i).getNodeValue());
      }
      catch (IOException ioe)
      {
         System.err.println("IOE: " + ioe);
      }
      catch (SAXException saxe)
      {
         System.err.println("SAXE: " + saxe);
      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (ParserConfigurationException pce)
      {
         System.err.println("PCE: " + pce);
      }
      catch (XPathException xpe)
      {
         System.err.println("XPE: " + xpe);
      }
   }
}

Listing 5-3.Locating Chicago Contacts with the XPath API

在解析了contacts.xml并构建了 DOM 树之后,main()通过调用它的XPathFactory newInstance()方法实例化了javax.xml.xpath.XPathFactory。产生的XPathFactory实例可以通过调用它的void setFeature(String name, boolean value)方法来设置特性(比如安全处理,安全地处理 XML 文档),通过调用它的XPath newXPath()方法来创建一个javax.xml.xpath.XPath对象,等等。

XPath声明了一个XPathExpression compile(String expression)方法,用于编译指定的expression(一个 XPath 表达式),并将编译后的表达式作为实现javax.xml.xpath.XPathExpression接口的类的实例返回。当表达式不能被编译时,这个方法抛出javax.xml.xpath.XPathExpressionException(一个javax.xml.xpath.XPathException的子类)。

XPath还声明了几个重载的evaluate()方法,用于立即计算表达式并返回结果。因为计算一个表达式需要时间,所以当您计划多次计算这个表达式时,您可能会选择先编译一个复杂的表达式(以提高性能)。

编译完表达式后,main()调用XPathExpressionObject evaluate(Object item, QName returnType)方法对表达式求值。第一个参数是表达式的上下文节点,在本例中恰好是一个Document实例。第二个参数指定了由evaluate()返回的对象的种类,并被设置为javax.xml.xpath.XPathConstants.NODESET,这是 XPath 1.0 节点集类型的限定名,通过 DOM 的NodeList接口实现。

Note

XPath API 将 XPath 的布尔、数字、字符串和节点集类型分别映射到 Java 的java.lang.Booleanjava.lang.Doublejava.lang.StringNodeList类型。当调用evaluate()方法时,通过XPathConstants常量(BOOLEANNUMBERSTRINGNODESET)指定 XPath 类型,该方法负责返回适当类型的对象。XPathConstants还声明了一个NODE常量,它不映射到 Java 类型。相反,它用来告诉evaluate()您只希望结果节点集包含单个节点。

在将Object转换为NodeList之后,main()使用这个接口的getLength()item()方法来遍历节点列表。对于这个列表中的每一项,调用getNodeValue()来返回节点的值,该值随后被输出。

编译清单 5-3 如下:

javac XPathSearch.java

运行生成的应用程序,如下所示:

java XPathSearch

您应该观察到以下输出:

John Doe
Bob Jones

高级 XPath

XPath API 提供了三个高级特性来克服 XPath 1.0 语言的局限性。这些特性是名称空间上下文、扩展函数和函数解析器,以及变量和变量解析器。

命名空间上下文

当 XML 文档的元素属于一个名称空间(包括默认名称空间)时,任何查询文档的 XPath 表达式都必须考虑这个名称空间。对于非默认的名称空间,表达式不需要使用相同的名称空间前缀;它只需要使用相同的 URI。但是,当文档指定默认名称空间时,即使文档不使用前缀,表达式也必须使用前缀。

为了理解这种情况,假设清单 5-1 的<contacts>标记被声明如下,以引入默认的名称空间:<contacts xmlns=" http://www.javajeff.ca/ ">。此外,假设清单 5-3 在实例化DocumentBuilderFactory的行之后包含了dbf.setNamespaceAware(true);。如果您要对修改后的contacts.xml文件运行修改后的XPathSearch应用程序,您将看不到任何输出。

您可以通过实现javax.xml.namespace.NamespaceContext将任意前缀映射到名称空间 URI 来纠正这个问题,然后用XPath实例注册这个名称空间上下文。清单 5-4 给出了NamespaceContext接口的最小实现。

import java.util.Iterator;

import javax.xml.XMLConstants;

import javax.xml.namespace.NamespaceContext;

public class NSContext implements NamespaceContext
{
   @Override
   public String getNamespaceURI(String prefix)
   {
      if (prefix == null)
         throw new IllegalArgumentException("prefix is null");
      else
      if (prefix.equals("tt"))
         return "http://www.javajeff.ca/";
      else
         return null;
   }

   @Override
   public String getPrefix(String uri)
   {
      return null;
   }

   @Override
   public Iterator getPrefixes(String uri)
   {
      return null;
   }
}

Listing 5-4.Minimally Implementing NamespaceContext

getNamespaceURI()方法传递一个必须映射到 URI 的prefix参数。当这个参数为null时,必须抛出一个java.lang.IllegalArgumentException对象(根据 Java 文档)。当参数是所需的前缀值时,将返回命名空间 URI。

在实例化了XPath类之后,通过调用XPathvoid setNamespaceContext(NamespaceContext nsContext)方法,实例化NSContext并向XPath对象注册该对象。例如,您在XPath xp = xpf.newXPath();之后指定xp.setNamespaceContext(new NSContext());来用xp注册NSContext对象。

剩下要做的就是将前缀应用到 XPath 表达式,该表达式现在变成了//tt:contact[tt:city='Chicago']/tt:name/text(),因为contactcityname元素现在是默认名称空间的一部分,其 URI 被映射到NSContext实例的getNamespaceURI()方法中的任意前缀tt

编译并运行修改后的XPathSearch应用程序,你会看到John DoeBob Jones在不同的行上。

扩展函数和函数解析器

XPath API 允许您定义函数(通过 Java 方法),通过提供尚未提供的新特性来扩展 XPath 的预定义函数集。这些 Java 方法不会有副作用,因为 XPath 函数可以按任意顺序计算多次。此外,它们不能覆盖预定义的函数;永远不会执行与预定义函数同名的 Java 方法。

假设您修改了清单 5-1 的 XML 文档,以包含一个birth元素,该元素以 YYYY-MM-DD 格式记录联系人的出生日期信息。清单 5-5 展示了生成的 XML 文件。

<?xml version="1.0"?>
<contacts >
   <contact>
      <name>John Doe</name>
      <birth>1953-01-02</birth>
      <city>Chicago</city>
      <city>Denver</city>
   </contact>
   <contact>
      <name>Jane Doe</name>
      <birth>1965-07-12</birth>
      <city>New York</city>
   </contact>
   <contact>
      <name>Sandra Smith</name>
      <birth>1976-11-22</birth>
      <city>Denver</city>
      <city>Miami</city>
   </contact>
   <contact>
      <name>Bob Jones</name>
      <birth>1958-03-14</birth>
      <city>Chicago</city>
   </contact>
</contacts>

Listing 5-5.XML-Based Contacts Database with Birth Information

现在假设您想根据出生信息选择联系人。例如,您只想选择出生日期大于1960-01-01的联系人。因为 XPath 没有为您提供这个函数,所以您决定声明一个date()扩展函数。你的第一步是声明一个实现了javax.xml.xpath.XPathFunction接口的Date类——参见清单 5-6 。

import java.text.ParsePosition;
import java.text.SimpleDateFormat;

import java.util.List;

import javax.xml.xpath.XPathFunction;
import javax.xml.xpath.XPathFunctionException;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class Date implements XPathFunction
{
   private final static ParsePosition POS = new ParsePosition(0);

   private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd");

   @Override
   public Object evaluate(List args) throws XPathFunctionException
   {
      if (args.size() != 1)
         throw new XPathFunctionException("Invalid number of arguments");
      String value;
      Object o = args.get(0);
      if (o instanceof NodeList)
      {

         NodeList list = (NodeList) o;
         value = list.item(0).getTextContent();
      }
      else
      if (o instanceof String)
         value = (String) o;
      else
         throw new XPathFunctionException("Cannot convert argument type");
      POS.setIndex(0);
      return sdf.parse(value, POS).getTime();
   }
}

Listing 5-6.An Extension Function for Returning a Date as a Milliseconds Value

XPathFunction声明了一个单独的Object evaluate(List args)方法,XPath 在需要执行扩展函数时会调用这个方法。向evaluate()传递一个java.util.List对象,这些对象描述由 XPath 计算器传递给扩展函数的参数。此外,这个方法返回一个适合扩展函数类型的值(date()的长整数返回类型与 XPath 的数字类型兼容)。

date()扩展函数旨在用单个参数调用,该参数可以是 nodeset 类型,也可以是 string 类型。当参数的数量(由列表的大小表示)不等于 1 时,这个扩展函数抛出javax.xml.xpath.XPathFunctionException

当参数类型为NodeList(节点集)时,获取节点集中第一个节点的文本内容;该内容被假定为 YYYY-MM-DD 格式的日期值(为了简洁,我忽略了错误检查)。当参数类型为String时,它被假定为这种格式的日期值。任何其他类型的参数都会导致抛出一个XPathFunctionException对象。

通过将日期转换为毫秒值,简化了日期比较。这个任务是在java.text.SimpleDateFormatjava.text.ParsePosition类的帮助下完成的。重置ParsePosition对象的索引(通过setIndex(0))后,调用SimpleDateFormatDate parse(String text, ParsePosition pos)方法根据SimpleDateFormat实例化时建立的模式解析字符串,从ParsePosition索引标识的解析位置开始。这个索引在parse()方法调用之前被重置,因为parse()更新了这个对象的索引。

parse()方法返回一个java.util.Date对象,该对象的long getTime()方法被调用以返回由解析的日期表示的毫秒数。

在实现扩展函数之后,您需要创建一个函数解析器,它是一个对象,其类实现了javax.xml.xpath.XPathFunctionResolver接口,并告诉 XPath 评估器关于扩展函数(或函数)。清单 5-7 展示了DateResolver类。

import javax.xml.namespace.QName;

import javax.xml.xpath.XPathFunction;
import javax.xml.xpath.XPathFunctionResolver;

public class DateResolver implements XPathFunctionResolver
{
   private static final QName name = new QName("http://www.javajeff.ca/",
                                               "date", "tt");

   @Override
   public XPathFunction resolveFunction(QName name, int arity)
   {
      if (name.equals(this.name) && arity == 1)
         return new Date();
      return null;
   }
}

Listing 5-7.A Function Resolver for the date() Extension Function

XPathFunctionResolver声明了一个单独的XPathFunction resolveFunction(QName functionName, int arity)方法,XPath 调用该方法来识别扩展函数的名称,并获得一个 Java 对象的实例,该对象的evaluate()方法实现了该函数。

functionName参数标识函数的限定名,因为所有的扩展函数必须存在于一个名称空间中,并且必须通过一个前缀来引用(该前缀不必与文档中的前缀匹配)。因此,您还必须通过名称空间上下文将名称空间绑定到前缀(如前所述)。arity参数标识扩展函数接受的参数数量,在重载扩展函数时非常有用。如果functionNamearity值可以接受,扩展函数的 Java 类被实例化并返回;否则,null就返回了。

最后,通过调用XPathvoid setXPathFunctionResolver(XPathFunctionResolver resolver)方法,用 XPath 对象实例化和注册函数解析器类。

以下摘自本章第 3 版的XPathSearch应用程序(在本书的代码档案中)演示了所有这些任务,以便在 XPath 表达式//tt:contact[tt:date(tt:birth) > tt:date('1960-01-01')]/tt:name/text()中使用date(),该表达式只返回出生日期大于 1960-01-01 ( Jane Doe后跟Sandra Smith)的联系人:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse("contacts.xml");
XPathFactory xpf = XPathFactory.newInstance();
XPath xp = xpf.newXPath();
xp.setNamespaceContext(new NSContext());
xp.setXPathFunctionResolver(new DateResolver());
XPathExpression xpe;
String expr; 

expr = "//tt:contact[tt:date(tt:birth) > tt:date('1960-01-01')]" +
       "/tt:name/text()";
xpe = xp.compile(expr);
Object result = xpe.evaluate(doc, XPathConstants.NODESET);
NodeList nl = (NodeList) result;
for (int i = 0; i < nl.getLength(); i++)
   System.out.println(nl.item(i).getNodeValue());

编译并运行修改后的XPathSearch应用程序,你会看到Jane DoeSandra Smith在不同的行上。

变量和变量解析器

之前指定的所有 XPath 表达式都是基于文本的。XPath 还允许您指定变量来参数化这些表达式,其方式类似于在 SQL 预准备语句中使用变量。

变量出现在表达式中的方式是在其名称(可能有也可能没有名称空间前缀)前加上一个$。例如,/a/b[@c = $d]/text()是一个 XPath 表达式,它选择根节点的所有a元素,以及具有包含由变量$d标识的值的c属性的所有ab元素,并返回这些b元素的文本。这个表达式对应于清单 5-8 的 XML 文档。

<?xml version="1.0"?>
<a>

   <b c="x">b1</b>
   <b>b2</b>
   <b c="y">b3</b>
   <b>b4</b>
   <b c="x">b5</b>
</a>
Listing 5-8.A Simple XML Document for Demonstrating an XPath Variable

要指定其值在表达式求值期间获得的变量,您必须用您的XPath对象注册一个变量解析器。变量解析器是一个类的实例,该类根据其Object resolveVariable(QName variableName)方法实现了javax.xml.xpath.XPathVariableResolver接口,并告诉求值器关于变量的信息。

variableName参数包含变量名的限定名。(请记住,变量名可能带有名称空间前缀。)此方法验证限定名是否恰当地命名了变量,然后返回它的值。

创建变量解析器后,通过调用XPathvoid setXPathVariableResolver(XPathVariableResolver resolver)方法,用XPath对象注册它。

以下节选自本章第 4 版的XPathSearch应用程序(在本书的代码档案中)演示了所有这些任务,以便在 XPath 表达式/a/b[@c=$d]/text()中指定$d,该表达式返回b1后跟b5。它假设清单 5-8 存储在名为example.xml的文件中:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse("example.xml");
XPathFactory xpf = XPathFactory.newInstance();
XPath xp = xpf.newXPath();
XPathVariableResolver xpvr;
xpvr = new XPathVariableResolver()
       {
          @Override
          public Object resolveVariable(QName varname)
          {
             if (varname.getLocalPart().equals("d"))
                return "x";
             else
                return null;
          }
       };
xp.setXPathVariableResolver(xpvr);
XPathExpression xpe;
xpe = xp.compile("/a/b[@c = $d]/text()");
Object result = xpe.evaluate(doc, XPathConstants.NODESET);
NodeList nl = (NodeList) result;
for (int i = 0; i < nl.getLength(); i++)
   System.out.println(nl.item(i).getNodeValue());

编译并运行修改后的XPathSearch应用程序,你会看到b1b5在不同的行上。

Caution

当您用名称空间前缀限定变量名时(如在$ns:d中),您还必须注册一个名称空间上下文来解析前缀。

Exercises

以下练习旨在测试你对第五章内容的理解。

Define XPath.   Where is XPath commonly used?   Identify the seven kinds of nodes that XPath recognizes.   True or false: XPath recognizes CDATA sections.   Describe what XPath provides for selecting nodes.   True or false: In a location path expression, you must prefix an attribute name with the @ symbol.   Identify the functions that XPath provides for selecting comment, text, and processing-instruction nodes.   What does XPath provide for selecting unknown nodes?   How do you perform multiple selections?   What is a predicate?   Identify the functions that XPath provides for working with nodesets.   Identify the three advanced features that XPath provides to overcome limitations with the XPath 1.0 language.   True or false: The XPath API maps XPath’s number type to java.lang.Float.   Modify Listing 5-1’s contacts document by changing <name>John Doe</name> to <Name>John Doe</Name>. Because you no longer see John Doe in the output when you run Listing 5-3’s XPathSearch application (you only see Bob Jones), modify this application’s location path expression so that you see John Doe followed by Bob Jones.  

摘要

XPath 是一种非 XML 声明性查询语言,用于选择 XML 文档的信息集项目作为一个或多个节点。它简化了对 DOM 树节点的访问,对于 XSLT 也很有用,XSLT 通常用于选择那些要复制到输出文档的输入文档元素(通过 XPath 表达式)。

XPath 将 XML 文档视为从根节点开始的节点树。这种语言识别七种节点:元素、属性、文本、名称空间、处理指令、注释和文档。它不识别 CDATA 节、实体引用或文档类型声明。

XPath 为选择节点提供了位置路径表达式。位置路径表达式通过从上下文节点(根节点或当前节点的其他文档节点)开始的一系列步骤来定位节点。返回的节点集(称为节点集)可能为空,也可能包含一个或多个节点。

位置路径表达式(返回节点集)是 XPath 表达式的一种。XPath 还支持计算结果为布尔值(比如谓词)、数字或字符串类型的通用表达式;比如position() = 26.8"Hello"。XSLT 中经常使用通用表达式。

XPath API 提供了一些高级功能来克服 XPath 1.0 语言的局限性:名称空间上下文(将任意名称空间前缀映射到名称空间 URIs)、扩展函数和函数解析器(用于定义扩展 XPath 预定义函数集的函数),以及变量和变量解析器(用于参数化 XPath 表达式)。

第六章向您介绍用于转换 XML 文档的 XSLT。

六、使用 XSLT 转换 XML 文档

除了 SAX、DOM、StAX 和 XPath,Java 还包括 XSLT API,用于转换 XML 文档。本章向您介绍 XSLT。

XSLT 是什么?

可扩展样式表语言(XSL)是用于转换和格式化 XML 文档的一系列语言。XSL 转换(XSLT)是用于将 XML 文档转换为其他格式的 XSL 语言,例如 HTML(用于通过 web 浏览器呈现 XML 文档的内容)。

XSLT 通过使用 XSLT 处理器和样式表来完成它的工作。XSLT 处理器是一种软件组件,它将 XSLT 样式表(一种由内容和转换指令组成的基于 XML 的模板)应用于输入文档(不修改文档),并将转换后的结果复制到结果树,该结果树可以输出到文件或输出流,甚至可以通过管道传输到另一个 XSLT 处理器进行其他转换。图 6-1 说明了转换过程。

A394211_1_En_6_Fig1_HTML.jpg

图 6-1。

An XSLT processor transforms an XML input document into a result tree

XSLT 的美妙之处在于,您不需要开发定制的软件应用程序来执行转换。相反,您只需创建一个 XSLT 样式表,并将其与需要转换到 XSLT 处理器的 XML 文档一起输入。

探索 XSLT API

Java 通过javax.xml.transformjavax.xml.transform.domjavax.xml.transform.saxjavax.xml.transform.staxjavax.xml.transform.stream包中的类型实现 XSLT。javax.xml.transform包定义了通用 API,用于处理转换指令和执行从源(XSLT 处理器的输入来源)到结果(发送处理器的输出)的转换。其余的包定义了获取不同种类的源和结果的 API。

javax.xml.transform.TransformerFactory类是使用 XSLT 的起点。通过调用它的一个newInstance()方法来实例化TransformerFactory。例如,下面的代码片段使用TransformerFactoryTransformerFactory newInstance()类方法来创建工厂:

TransformerFactory tf = TransformerFactory.newInstance();

在幕后,newInstance()遵循一个有序的查找过程来识别要加载的TransformerFactory实现类。这个过程首先检查javax.xml.transform.TransformerFactory系统属性,最后在找不到其他类时选择 Java 平台的默认TransformerFactory实现类。如果一个实现类不可用(也许由javax.xml.transform.TransformerFactory系统属性标识的类不存在)或者不能被实例化,newInstance()抛出一个javax.xml.transform.TransformerFactoryConfigurationError类的实例。否则,它实例化该类并返回其实例。

获得一个TransformerFactory对象后,可以调用各种配置方法来配置工厂。例如,您可以调用TransformerFactoryvoid setFeature(String name, boolean value)方法来启用一个特性(比如安全处理,安全地转换 XML 文档)。

按照工厂的配置,调用它的一个newTransformer()方法来创建和返回javax.xml.transform.Transformer类的实例。下面的代码片段调用Transformer newTransformer()来完成这个任务:

Transformer t = tf.newTransformer();

noargument newTransformer()方法将源输入复制到目标,不做任何修改。这种转换被称为身份转换。

要更改输入,请指定一个样式表。通过调用工厂的Transformer newTransformer(Source source)方法来完成这项任务,其中的javax.xml.transform.Source接口描述了样式表的来源。以下代码片段完成了这项任务:

Transformer t;
t = tf.newTransformer(new StreamSource(new FileReader("recipe.xsl")));

这段代码创建了一个转换器,它通过一个连接到文件阅读器的javax.xml.transform.stream.StreamSource对象从名为recipe.xsl的文件中获取样式表。习惯上使用.xsl.xslt扩展名来标识 XSLT 样式表文件。

newTransformer()方法不能返回对应于工厂配置的Transformer实例时,它们抛出javax.xml.transform.TransformerConfigurationException

在获得一个Transformer实例之后,您可以调用它的void setOutputProperty(String name, String value)方法来影响一个转换。javax.xml.transform.OutputKeys类声明了常用键的常量。例如,OutputKeys.METHOD是指定输出结果树的方法的键(如 XML、HTML、纯文本或其他)。

Tip

要在一个方法调用中设置多个属性,创建一个java.util.Properties对象并将该对象作为参数传递给Transformervoid setOutputProperties(Properties prop)方法。由setOutputProperty()setOutputProperties()设置的属性覆盖样式表的xsl:output指令设置。

在执行转换之前,您需要获得实现Sourcejavax.xml.transform.Result接口的类的实例。然后将这些实例传递给Transformervoid transform(Source xmlSource, Result outputTarget)方法,当转换过程中出现问题时,该方法抛出一个javax.xml.transform.TransformerException类的实例。

以下代码片段向您展示了如何获取源和结果,以及如何执行转换:

Source source = new DOMSource(doc);
Result result = new StreamResult(System.out);
t.transform(source, result);

第一行实例化了javax.xml.transform.dom.DOMSource类,它作为一个 DOM 树的持有者,这个 DOM 树植根于由doc指定的org.w3c.dom.Document对象。第二行实例化了javax.xml.transform.stream.StreamResult类,它充当标准输出流的容器,转换后的数据项被发送到该输出流。第三行从Source对象读取数据,并将转换后的数据输出到Result对象。

Transformer Factory Feature Detection

尽管 Java 的默认转换器支持位于javax.xml.transform.domjavax.xml.transform.saxjavax.xml.transform.staxjavax.xml.transform.stream包中的各种SourceResult实现类,但非默认转换器(可能通过javax.xml.transform.TransformerFactory系统属性指定)可能更受限制。因此,每个SourceResult实现类都声明了一个FEATURE字符串常量,可以传递给TransformerFactoryboolean getFeature(String name)方法。当支持SourceResult实现类时,该方法返回true。例如,当支持流源时,tf.getFeature(StreamSource.FEATURE)返回true

javax.xml.transform.sax.SAXTransformerFactory类提供了额外的特定于 SAX 的工厂方法,只有当TransformerFactory对象也是该类的实例时才能使用这些方法。为了帮助您做出决定,SAXTransformerFactory还声明了一个FEATURE字符串常量,您可以将它传递给getFeature()。例如,当从tf引用的 transformer factory 是SAXTransformerFactory的实例时,tf.getFeature(SAXTransformerFactory.FEATURE)返回true

大多数 XML API 接口对象和返回它们的工厂都不是线程安全的。这种情况也适用于变压器。尽管您可以在同一线程上多次重用同一个转换器,但是您不能从多个线程访问该转换器。

这个问题可以通过使用实现javax.xml.transform.Templates接口的类的实例来解决。该接口的 Java 文档是这样说的:对于并发运行的多个线程上的给定实例,模板必须是线程安全的,并且可以在给定的会话中多次使用。除了促进线程安全之外,Templates实例还可以提高性能,因为它们代表编译的 XSLT 样式表。

下面的代码片段展示了如何在没有Templates对象的情况下执行转换:

TransformerFactory tf = TransformerFactory.newInstance();
StreamSource ssStyleSheet = new StreamSource(new FileReader("recipe.xsl"));
Transformer t = tf.newTransformer(ssStyleSheet);
t.transform(new DOMSource(doc), new StreamResult(System.out));

您不能从多个线程访问t的转换器。相比之下,下面的代码片段向您展示了如何从一个Templates对象构建一个转换器,以便可以从多个线程访问它:

TransformerFactory tf = TransformerFactory.newInstance();
StreamSource ssStyleSheet = new StreamSource(new FileReader("recipe.xsl"));
Templates te = tf.newTemplates(ssStylesheet);
Transformer t = te.newTransformer();
t.transform(new DOMSource(doc), new StreamResult(System.out));

不同之处在于调用TransformerfactoryTemplates newTemplates(Source source)方法来创建并返回其类实现了Templates接口的对象,以及调用该接口的Transformer newTransformer()方法来获得Transformer对象。

演示 XSLT API

清单 3-2 展示了一个DOMDemo应用程序,它基于清单 1-2 的电影 XML 文档创建了一个 DOM 文档树。不幸的是,您不能使用 DOM API 将ISO-8859-1赋给 XML 声明的encoding属性。此外,您不能使用 DOM 将该树输出到文件或其他目的地。然而,您可以用 XSLT 克服这些问题,如清单 6-1 所示。

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;

import javax.xml.transform.dom.DOMSource;

import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;

public class XSLTDemo
{
   public static void main(String[] args)
   {
      try
      {
         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         DocumentBuilder db = dbf.newDocumentBuilder();
         Document doc = db.newDocument();
         doc.setXmlStandalone(true);
         // Create the root element.
         Element root = doc.createElement("movie");
         doc.appendChild(root);
         // Create name child element and add it to the root.
         Element name = doc.createElement("name");
         root.appendChild(name);
         // Add a text element to the name element.
         Text text =
           doc.createTextNode("Le Fabuleux Destin d'Amélie Poulain");
         name.appendChild(text);
         // Create language child element and add it to the root.
         Element language = doc.createElement("language");
         root.appendChild(language);
         // Add a text element to the language element.
         text = doc.createTextNode("français");
         language.appendChild(text);
         // Use a transformer to output this tree with ISO-8859-1 encoding
         // to the standard output stream.
         TransformerFactory tf = TransformerFactory.newInstance();
         Transformer t = tf.newTransformer();
         t.setOutputProperty(OutputKeys.METHOD, "xml");
         t.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1");
         t.setOutputProperty(OutputKeys.INDENT, "yes");
         t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "3");
         Source source = new DOMSource(doc);
         Result result = new StreamResult(System.out);
         t.transform(source, result);
      }

      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (ParserConfigurationException pce)
      {
         System.err.println("PCE: " + pce);
      }
      catch (TransformerConfigurationException tce)
      {
         System.err.println("TCE: " + tce);
      }
      catch (TransformerException te)
      {
         System.err.println("TE: " + te);
      }
      catch (TransformerFactoryConfigurationError tfce)
      {
         System.err.println("TFCE: " + tfce);
      }
   }
}

Listing 6-1.Assigning ISO-8859-1 to the XML declaration’s encoding Attribute via XSLT

清单 6-1 首先创建一个 DOM 树。然后,它创建一个转换器工厂,并从这个工厂获得一个转换器。然后在 transformer 上设置四个属性,并获得一个流源和结果。最后,调用transform()方法将源内容转换成结果。

在转换器上设置的四个属性会影响转换。OutputKeys.METHOD指定结果树将被写成 XML,OutputKeys.ENCODING指定ISO-8859-1将是 XML 声明的encoding属性的值,OutputKeys.INDENT指定转换器可以输出额外的空白。

额外的空白用于跨多行输出 XML,而不是在一行中输出。因为指出用于缩进 XML 行的空格数会很好,并且因为该信息不能通过OutputKeys属性来指定,所以使用非标准的"{ http://xml.apache.org/xslt}indent-amount "属性(属性键以大括号分隔的 URIs 开始)来指定适当的值(例如3空格)。在这个应用程序中指定这个属性是可以的,因为 Java 的默认 XSLT 实现是基于 Apache 的 XSLT 实现的。

编译清单 6-1 如下:

javac XSLTDemo.java

运行生成的应用程序,如下所示:

java XSLTDemo

您应该观察到以下输出:

<?xml version="1.0" encoding="ISO-8859-1"?><movie>
   <name>Le Fabuleux Destin d'Amélie Poulain</name>
   <language>français</language>
</movie>

虽然这个例子向您展示了如何输出一个 DOM 树以及如何为结果 XML 文档的 XML 声明指定一个encoding值,但是这个例子并没有真正展示 XSLT 的强大功能,因为(除了设置encoding属性值之外)它执行了一个身份转换。一个更有趣的例子是利用样式表。

考虑这样一个场景,您想要将清单 1-1 的食谱文档转换成 HTML 文档,以便通过 web 浏览器呈现。清单 6-2 展示了一个样式表,转换器可以用它来执行转换。

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/recipe">
<html>
   <head>
      <title>Recipes</title>
   </head>

   <body>
      <h2>

         <xsl:value-of select="normalize-space(title)"/>
      </h2>

      <h3>Ingredients</h3>

      <ul>
      <xsl:for-each select="ingredients/ingredient">
        <li>
           <xsl:value-of select="normalize-space(text())"/>
           <xsl:if test="@qty"> (<xsl:value-of select="@qty"/>)</xsl:if>
        </li>
      </xsl:for-each>
      </ul>

      <h3>Instructions</h3>

      <xsl:value-of select="normalize-space(instructions)"/>
   </body>
</html>
</xsl:template>
</xsl:stylesheet>

Listing 6-2.An XSLT Stylesheet for Converting a Recipe Document to an HTML Document

清单 6-2 揭示了样式表是一个 XML 文档。它的根元素是stylesheet,标识样式表的标准名称空间。习惯上指定xsl作为引用 XSLT 指令元素的名称空间前缀,尽管可以指定任何前缀。

样式表基于控制元素及其内容如何转换的template元素。模板关注通过match属性识别的单个元素。这个属性的值是一个 XPath 位置路径表达式,它匹配根元素节点的所有recipe子节点。关于清单 1-1 ,将只匹配和选择单个recipe根元素。

一个template元素可以包含文字文本和样式表指令。例如,<xsl:value-of select="normalize-space(title)"/>中的value-of指令指定检索title元素的值(它是recipe上下文节点的子节点)并将其复制到输出中。因为该文本被空格和换行符包围,所以在复制标题之前,调用 XPath 的normalize-string()函数来删除这些空格。

XSLT 是一种功能强大的声明性语言,包括控制流指令,如for-eachif。在<xsl:for-each select="ingredients/ingredient">的上下文中,for-each使得ingredients节点的所有ingredient子节点被一次一个地选择和处理。对于每个节点,执行<xsl:value-of select="normalize-space(text())"/>来复制ingredient节点的内容,规范化以删除空白。另外,<xsl:if test="@qty"> (<xsl:value-of select="@qty"/>)中的if指令确定ingredient节点是否有一个qty属性,并且(如果有)将一个空格字符和该属性值(用括号括起来)复制到输出中。

Note

XSLT 还有很多内容无法在这个简短的例子中展示。要了解更多关于 XSLT 的知识,我建议您阅读《从新手到专业人员的 XSLT 2.0 入门》( www.apress.com/9781590593240 ),这是一本由林洋·坦尼森撰写的新书。XSLT 2.0 是 XSLT 1.0 的超集,Java 8 支持 XSLT 1.0。

清单 6-3 将源代码呈现给一个XSLTDemo应用程序,该应用程序向您展示如何编写 Java 代码来通过清单 6-2 的样式表处理清单 1-1 。

import java.io.FileReader;
import java.io.IOException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;

import javax.xml.transform.dom.DOMSource;

import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.w3c.dom.Document;

import org.xml.sax.SAXException;

public class XSLTDemo
{
   public static void main(String[] args)
   {
      try
      {
         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         DocumentBuilder db = dbf.newDocumentBuilder();
         Document doc = db.parse("recipe.xml");
         TransformerFactory tf = TransformerFactory.newInstance();
         StreamSource ssStyleSheet;
         ssStyleSheet = new StreamSource(new FileReader("recipe.xsl"));
         Transformer t = tf.newTransformer(ssStyleSheet);
         t.setOutputProperty(OutputKeys.METHOD, "html");
         t.setOutputProperty(OutputKeys.INDENT, "yes");
         Source source = new DOMSource(doc);
         Result result = new StreamResult(System.out);
         t.transform(source, result);
      }

      catch (IOException ioe)
      {
         System.err.println("IOE: " + ioe);
      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (ParserConfigurationException pce)
      {
         System.err.println("PCE: " + pce);
      }
      catch (SAXException saxe)
      {
         System.err.println("SAXE: " + saxe);
      }
      catch (TransformerConfigurationException tce)
      {
         System.err.println("TCE: " + tce);
      }
      catch (TransformerException te)
      {
         System.err.println("TE: " + te);
      }
      catch (TransformerFactoryConfigurationError tfce)
      {
         System.err.println("TFCE: " + tfce);
      }
   }
}

Listing 6-3.Transforming Recipe XML via a Stylesheet

清单 6-3 在结构上与清单 6-1 相似。它揭示了输出方法被设置为html,也揭示了结果 HTML 应该缩进。但是,输出只是部分缩进,如下所示:

<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Recipes</title>
</head>

<body>
<h2>Grilled Cheese Sandwich</h2>
<h3>Ingredients</h3>
<ul>
<li>bread slice (2)</li>
<li>cheese slice</li>
<li>margarine pat (2)</li>
</ul>
<h3>Instructions</h3>Place frying pan on element and select medium heat. For each bread slice, smear one pat of margarine on one side of bread slice. Place cheese slice between bread slices with margarine-smeared sides away from the cheese. Place sandwich in frying pan with one margarine-smeared side in contact with pan. Fry for a couple of minutes and flip. Fry other side for a minute and serve.</body>
</html>

OutputKeys.INDENT和它的"yes"值允许你跨多行输出 HTML,而不是在一行中输出 HTML。但是,XSLT 处理器不执行额外的缩进,并忽略通过代码(如t.setOutputProperty("{ http://xml.apache.org/xslt}indent-amount ", "3");)指定缩进的空格数的尝试。

Note

OutputKeys.METHOD被设置为"html"时,XSLT 处理器输出一个<META>标签。

Exercises

以下练习旨在测试您对第六章内容的理解:

Define XSLT.   How does XSLT accomplish its work?   True or false: Call TransformerFactory’s void transform(Source xmlSource, Result outputTarget) method to transform a source to a result.   Create a books.xsl stylesheet file and a MakeHTML application with a similar structure to the application that processes Listing 6-2’s recipe.xsl stylesheet. MakeHTML uses books.xsl to convert Exercise 1-21’s books.xml content to HTML. When viewed in a web browser, the HTML should result in a web page that’s similar to the page shown in Figure 6-2.

A394211_1_En_6_Fig2_HTML.jpg

图 6-2。

Exercise 1-21’s books.xml content is presented via a web page  

摘要

XSL 是用于转换和格式化 XML 文档的一系列语言。XSLT 是用于将 XML 文档转换成其他格式的 XSL 语言,例如 HTML(用于通过 web 浏览器呈现 XML 文档的内容)。

XSLT 通过使用 XSLT 处理器和样式表来完成它的工作。XSLT 处理器将 XSLT 样式表应用于输入文档(不修改文档),并将转换后的结果复制到结果树,结果树可以输出到文件或输出流,甚至可以通过管道传输到另一个 XSLT 处理器进行其他转换。

Java 通过javax.xml.transformjavax.xml.transform.domjavax.xml.transform.saxjavax.xml.transform.staxjavax.xml.transform.stream包中的类型实现 XSLT。javax.xml.transform包定义了通用 API,用于处理转换指令和执行从源(XSLT 处理器的输入来源)到结果(发送处理器的输出)的转换。其余的包定义了获取不同种类的源和结果的 API。

第七章向您介绍 JSON,一种不太冗长的 XML 替代品。

七、JSON 简介

许多应用程序通过交换 JSON 对象而不是 XML 文档来进行通信。本章介绍 JSON,浏览它的语法,在 JavaScript 上下文中演示 JSON,并展示如何在 JSON 模式的上下文中验证 JSON 对象。

JSON 是什么?

JSON (JavaScript Object Notation)是一种独立于语言的数据格式,它将 JSON 对象表示为人类可读的属性列表(名称-值对)。尽管源自 JavaScript 的非严格子集,但将JSON 对象解析成等价的语言相关对象的代码在许多编程语言中都可用。

Note

JSON 允许 Unicode U+2028行分隔符和U+2029段落分隔符在带引号的字符串中不转义。因为 JavaScript 不支持这种能力,所以 JSON 不是 JavaScript 的子集。

JSON 常用于通过 Ajax(https://en.wikipedia.org/wiki/AJAJ)进行的异步浏览器/服务器通信。JSON 也用于 NoSQL 数据库管理系统,如 MongoDb 和 CouchDb 使用 Twitter、脸书、LinkedIn 和 Flickr 等社交媒体网站的应用程序;即使使用流行的谷歌地图 API。

Note

许多开发人员更喜欢 JSON 而不是 XML,因为他们认为 JSON 不那么冗长,更容易阅读。查看“JSON:XML 的无脂肪替代品”( www.json.org/xml.html )了解更多信息。

JSON 语法教程

JSON 数据格式将 JSON 对象表示为用大括号分隔、用逗号分隔的属性列表:

{
   property1 ,
   property2 ,
   ...
   propertyN

}

最后一个属性后没有逗号。

对于每个属性,名称被表示为一个通常用引号括起来的字符串(用一对双引号括起来)。名称字符串后面跟一个冒号字符,后面跟一个特定类型的值(例如,"name": "JSON")。

JSON 支持以下六种类型:

  • Number:一个有符号的十进制数,可以包含小数部分,可以使用指数( E)符号。JSON 不允许非数字(比如 NaN ),也不区分整数和浮点。此外,JSON 不识别八进制和十六进制格式。(尽管 JavaScript 对所有数值都使用了一种双精度浮点格式,但是实现 JSON 的其他语言对数字的编码可能不同。)
  • 字符串:零个或多个 Unicode 字符的序列。字符串用双引号分隔,并支持反斜杠转义语法。
  • 布尔:值truefalse中的任意一个。
  • 数组:零个或多个值的有序列表,每个值可以是任意类型。数组使用方括号符号,元素用逗号分隔。
  • 对象:属性的无序集合,其中的名称(也称为键)是字符串。因为对象旨在表示关联数组,所以建议(尽管不是必需的)每个键在一个对象中是唯一的。对象用大括号分隔,并用逗号分隔每个属性。在每个属性中,冒号字符将键与其值分开。
  • Null :空值,使用关键字null

Note

JSON 模式(稍后讨论)识别第七种类型:整数。这种类型不包括分数或指数,而是数字的子集。

语法元素(值和标点符号)周围或之间允许有空格,但会被忽略。为此,四个特定字符被视为空白:空格、水平制表符、换行符和回车符。还有,JSON 不支持注释。

使用这种数据格式,您可以指定一个 JSON 对象,如下面的匿名对象(节选自维基百科的 JSON 页面上的 https://en.wikipedia.org/wiki/JSON )来描述一个人的名字、姓氏和其他数据项:

{
   "firstName": "John",
   "lastName": "Smith",
   "isAlive": true,
   "age": 25,
   "address":
   {
      "streetAddress": "21 2nd Street",
      "city": "New York",
      "state": "NY",
      "postalCode": "10021-3100"
   },
   "phoneNumbers":
   [
      {
         "type": "home",
         "number": "212 555-1234"
      },
      {
         "type": "office",
         "number": "646 555-4567"
      }
   ],
   "children": [],
   "spouse": null
}

在此示例中,匿名对象由带有以下键的八个属性组成:

  • firstName标识一个人的名字,类型为 string。
  • lastName标识一个人的姓,类型为 string。
  • isAlive标识一个人的存活状态,属于布尔类型。
  • age识别一个人的年龄,属于数字类型。
  • address标识一个人的位置,属于 object 类型。在这个对象中有四个属性(字符串类型):streetAddresscitystatepostalCode
  • 识别一个人的电话号码,并且是数组类型。数组中有两个对象;每个对象由typenumber属性(字符串类型)组成。
  • 标识一个人的孩子(如果有的话),并且是数组类型。
  • spouse标识一个人的伴侣,为空。

前面的示例显示了对象和数组可以嵌套;例如,对象内数组中的对象。

Note

按照惯例,JSON 对象存储在扩展名为.json的文件中。

用 JavaScript 演示 JSON

理想情况下,我会用 Java 的标准 JSON API 来演示 JSON。然而,Java 并不正式支持 JSON。

Note

Oracle 之前推出了一个 Java 增强提案(JEP ),将 JSON API 添加到 Java 9 中。不幸的是,JEP 198:轻量级 JSON API ( http://openjdk.java.net/jeps/198 )被放弃了。

我将通过 JavaScript 演示 JSON,但是是在 Java 环境中通过 Java 的脚本 API。(如果您不熟悉脚本,我将解释这个 API,以便您能够理解代码。)首先,清单 7-1 展示了执行 JavaScript 代码的应用程序的源代码。

import java.io.FileReader;
import java.io.IOException;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class RunScript
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
         System.err.println("usage: java RunScript script");
         return;
      }
      ScriptEngineManager manager = new ScriptEngineManager();
      ScriptEngine engine = manager.getEngineByName("nashorn");
      try
      {
         engine.eval(new FileReader(args[0]));
      }
      catch (ScriptException se)
      {
         System.err.println(se.getMessage());
      }
      catch (IOException ioe)
      {
         System.err.println(ioe.getMessage());
      }      
   }
}

Listing 7-1.Executing JavaScript Code with Assistance from Java

清单 7-1 的main()方法首先验证是否指定了一个命令行参数,该参数命名了一个脚本文件。如果不是这样,它会显示使用信息并终止应用程序。

假设指定了一个命令行参数,javax.script.ScriptEngineManager类被实例化。ScriptEngineManager作为脚本 API 的入口点。

接下来,ScriptEngineManager对象的ScriptEngine getEngineByName(String shortName)方法被调用以获得对应于期望的shortName值的脚本引擎。JavaScript 支持两个脚本引擎:rhinonashorn。我选择获得更现代的nashorn脚本引擎,它作为一个对象返回,该对象的类实现了javax.script.ScriptEngine接口。

ScriptEngine声明了几个用于评估脚本的eval()方法。main()调用Object eval(Reader reader)方法从其java.io.FileReader对象参数中读取脚本,然后(假设java.io.IOException没有被抛出)评估脚本。这个方法返回任何脚本返回值,我忽略了。此外,当脚本中出现错误时,该方法抛出javax.script.ScriptException

编译清单 7-1 如下:

javac RunScript.java

在运行这个应用程序之前,您需要一个合适的脚本文件。清单 7-2 展示了一个声明和访问 JSON 对象的脚本。

var person =
{
   "firstName": "John",
   "lastName": "Smith",
   "isAlive": true,
   "age": 25,
   "address":
   {
      "streetAddress": "21 2nd Street",
      "city": "New York",
      "state": "NY",
      "postalCode": "10021-3100"
   },
   "phoneNumbers":
   [
      {
         "type": "home",
         "number": "212 555-1234"
      },
      {
         "type": "office",
         "number": "646 555-4567"
      }
   ],
   "children": [],
   "spouse": null
};
print(person.firstName);
print(person.lastName);
print(person.address.city);
print(person.phoneNumbers[1].number);
Listing 7-2.Declaring and Accessing a Person Object

假设清单 7-2 存储在person.js中,运行应用程序如下:

java RunScript person.js

您应该观察到以下输出:

John
Smith
New York
646 555-4567

JSON 对象作为独立于语言的文本存在。要将文本转换成语言相关的对象,需要解析文本。JavaScript 为这个任务提供了一个带有parse()方法的JSON对象。将要解析的文本作为参数传递给parse(),并接收生成的基于 JavaScript 的对象作为该方法的返回值。parse()当文本不符合 JSON 格式时抛出SyntaxError

清单 7-3 展示了一个演示parse()的脚本。

var creditCardText =
"{ \"number\": \"1234567890123456\", \"expiry\": \"04/20\", \"type\": " +
"\"visa\" }";
var creditCard = JSON.parse(creditCardText);
print(creditCard.number);
print(creditCard.expiry);
print(creditCard.type);

var creditCardText2 = "{ 'type': 'visa' }";
var creditCard2 = JSON.parse(creditCardText2);

Listing 7-3.Parsing a JSON Object

假设清单 7-3 存储在cc.js中,运行应用程序如下:

java RunScript cc.js

您应该观察到以下输出:

1234567890123456
04/20
visa
SyntaxError: Invalid JSON: <json>:1:2 Expected , or } but found '
{ 'type': 'visa' }
  ^ in <eval> at line number 10

语法错误表明不能用单引号分隔名称。

关于在 JavaScript 环境中使用 JSON,我要说的就是这些。因为这本书是以 Java 为中心的,后续章节将探讨各种第三方 Java APIs,用于将 JSON 对象解析成 Java 相关对象,反之亦然。

验证 JSON 对象

应用程序经常需要验证 JSON 对象,以确保所需的属性存在,并且满足附加的约束条件(比如价格不能低于一美元)。验证通常在 JSON 模式的上下文中执行。

JSON Schema 是一种语法语言,用于定义 JSON 对象的结构、内容和(在某种程度上)语义。它允许您指定关于对象属性含义的元数据(关于数据的数据)以及这些属性的有效值。应用语法语言的结果是一个模式(蓝图),它描述了根据模式有效的 JSON 对象集。

Note

JSON 模式将模式表示为 JSON 对象。

JSON 模式在 JSON 模式网站( http://json-schema.org )上维护。这个网站揭示了 JSON 模式的几个优点:

  • 它描述了您现有的数据格式。
  • 它提供了清晰的、人类可读的和机器可读的文档。
  • 它提供了完整的结构化验证,这对于自动化测试和验证客户提交的数据非常有用。

Note

JSON 模式网站主要关注 JSON 模式规范的草案版本 4。该规范分为三个部分:JSON 模式核心、JSON 模式验证和 JSON 超级模式。

要理解 JSON 模式,请考虑以下 JSON 对象:

{
   "name": "John Doe",
   "age": 35
}

这个对象用一个name和一个age来描述一个人。让我们设置以下约束:两个属性都必须存在,name必须是字符串类型,age必须是数字类型,age的值必须在 18 到 64 之间。

以下模式(基于 JSON 模式的草案版本 4)为该对象提供了必要的约束:

{
   "$schema": "http://json-schema.org/draft-04/schema#",
   "title": "Person",
   "description": "A person",
   "type": "object",
   "properties":
   {
      "name":
      {
         "description": "A person's name",
         "type": "string"
      },
      "age":
      {
         "description": "A person's age",
         "type": "number",
         "minimum": 18,
         "maximum": 64
      }

   },
   "required": ["name", "age"]    
}

从上到下阅读,您会将这个基于 JSON 的模式解释如下:

  • 关键字$schema声明该模式是根据草案版本 4 规范编写的。
  • title关键字标识这个模式正在验证的 JSON 对象。在这种情况下,正在验证一个Person对象。
  • description关键字提供了对Person对象的描述。与title一样,description没有给被验证的数据添加任何约束。
  • type关键字表示包含对象是一个 JSON 对象(通过object值)。此外,它还识别属性类型(例如stringnumber)。
  • 关键字properties引入了一个可以出现在 JSON 对象中的属性数组。这些属性被标识为nameage。每个属性由一个对象进一步描述,该对象提供了一个描述属性的description关键字和一个识别可以分配给属性的值的类型的type关键字。这是一个约束:您必须给name分配一个字符串,给age分配一个数字。对于age属性,minimummaximum关键字被指定来提供额外的约束:分配给age的数字必须在从1864的范围内。
  • required关键字引入了一个数组,该数组标识那些必须存在于 JSON 对象中的属性。在这个例子中,nameage都是必需的属性。

JSON 模式网站提供了不同编程语言的各种验证器实现的链接(参见 http://json-schema.org/implementations.html )。您可以下载一个实现,并将其集成到您的应用程序中,但须符合许可要求。对于这一章,我选择使用一个名为 JSON Schema Lint ( http://jsonschemalint.com/draft4/ )的在线工具来演示验证。

图 7-1 显示了 JSON Schema Lint 在线工具的相应窗口中先前的 JSON 对象和模式。

A394211_1_En_7_Fig1_HTML.jpg

图 7-1。

The schema is valid and the JSON object conforms to this schema

让我们对 JSON 对象进行一些修改,使它不再符合模式,并看看 JSON 模式 Lint 工具如何响应。首先,让我们将65赋值给age,这超过了age属性的maximum约束。图 7-2 显示了结果。

A394211_1_En_7_Fig2_HTML.jpg

图 7-2。

JSON Schema Lint changes its header color to red to signify an error, and also identifies the property and constraint that’s been violated

接下来,让我们将age的值恢复为35,但是用双引号将它括起来,这将类型从数字更改为字符串。结果如图 7-3 所示。

A394211_1_En_7_Fig3_HTML.jpg

图 7-3。

JSON Schema Lint reports that the age property has the wrong type

最后,让我们将age的值恢复为35,但是删除name属性。图 7-4 显示了 JSON 模式 Lint 的响应。

A394211_1_En_7_Fig4_HTML.jpg

图 7-4。

JSON Schema Lint reports that the name property is required Note

查看“JSON 模式:核心定义和术语”( http://json-schema.org/latest/json-schema-core.html )和“JSON 模式:交互式和非交互式验证”( http://json-schema.org/latest/json-schema-validation.html )文档,了解更多关于创建基于 JSON 模式的模式的信息。

Exercises

以下练习旨在测试你对第七章内容的理解。

Define JSON.   True or false: JSON is derived from a strict subset of JavaScript.   How does the JSON data format present a JSON object?   Identify the six types that JSON supports.   True or false: JSON doesn’t support comments.   How would you parse a JSON object into an equivalent JavaScript object?   Define JSON Schema.   When creating a schema, how do you identify those properties that must be present in those JSON objects that the schema validates?   Declare a JSON object for a product in terms of name and price properties. Set the name to "hammer" and the price to 20.   Declare a schema for validating the previous JSON object. The schema should constrain name to be a string, price to be a number, price to be at least 1 dollar, and name and price to be present in the object. Use JSON Schema Lint to verify the schema and JSON object.  

摘要

JSON 是一种独立于语言的数据格式,它将 JSON 对象表示为人类可读的属性列表。虽然源自 JavaScript,但是将JSON 对象解析成等价的语言相关对象的代码在许多编程语言中都有。

JSON 数据格式将 JSON 对象表示为用大括号分隔、用逗号分隔的属性列表。对于每个属性,名称都表示为双引号字符串。名称字符串后面跟一个冒号字符,后面跟一个特定 JSON 类型的值。

应用程序经常需要验证 JSON 对象,以确保所需的属性存在,并且满足附加的约束条件(比如价格不能低于一美元)。JSON Schema 是一种让您完成验证的语法语言。

第八章介绍了用于解析和创建 Json 对象的 mJson。

八、使用 mJson 解析和创建 JSON 对象

许多第三方 API 可用于解析和创建 JSON 对象。本章探索这些 API 中最简单的一个:mJson。

mJson 是什么?

mJson 是一个基于 Java 的小型 Json 库,用于将 JSON 对象解析成 Java 对象,反之亦然。mJson 提供以下功能:

  • 单一通用类型(一切都是一个Json对象;没有类型转换)
  • 创建Json对象的方法
  • 了解Json物体的方法
  • 导航Json对象层次的方法
  • 修改Json对象的方法
  • 完全支持 JSON 模式草案 4 验证
  • 用于增强的可插拔工厂Json
  • 实现更紧凑代码的方法链接
  • 快速的手工编码解析
  • 整个库包含在一个 Java 源文件中

与其他 Json 库不同,mJson 专注于在 Java 中操作 JSON 结构,而不是将它们映射到强类型 Java 对象。因此,mJson 减少了冗长,让您在 Java 中像在 JavaScript 中一样自然地使用 Json。

Note

mJson 是由开发者 Borislav Lordanov 创建的。这个库位于 GitHub 的 http://bolerio.github.io/mjson/

获取和使用 mJson

mJson 作为单个 Jar 文件分发;mjson-1.3.jar是编写时最新的 Jar 文件。要获取这个 Jar 文件,请将浏览器指向 http://repo1.maven.org/maven2/org/sharegov/mjson/1.3/mjson-1.3.jar

mjson-1.3.jar包含一个Json类文件和其他描述嵌套在Json类中的包私有类的类文件。此外,这个 Jar 文件显示Json位于mjson包中。

Note

mJson 根据 Apache License 版( www.apache.org/licenses/ )进行许可。

mjson-1.3.jar很容易。编译源代码或运行应用程序时,只需将它包含在类路径中,如下所示:

javac -cp mjson-1.3.jar source file

java -cp mjson-1.3.jar;. main 
classfile

探索 Json 类

Json类描述了一个 JSON 对象或 JSON 对象的一部分。它包含了SchemaFactory接口,50 多个方法和其他成员。本节将探讨这些方法中的大部分以及SchemaFactory

Note

Json类的 API 文档位于 http://bolerio.github.io/mjson/apidocs/index.html

创建 Json 对象

Json声明了几个创建和返回Json对象的static方法。其中三种方法读取并解析外部 JSON 对象:

  • Json read(String s):从传递给s(类型为java.lang.String)的字符串中读取一个 JSON 对象,并解析这个对象。
  • Json read(URL url):从传递给type java.net.URLurl的统一资源定位符(URL)中读取一个 JSON 对象,并解析这个对象。
  • Json read(CharacterIterator ci):从传递给ci(类型java.text.CharacterIterator)的字符迭代器中读取一个 JSON 对象,并解析该对象。

每个方法都返回一个描述被解析的 JSON 对象的Json对象。

清单 8-1 展示了演示read(String)方法的应用程序的源代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"lastName\": \"Smith\"," +
      "\"isAlive\": true," +
      "\"age\": 25," +
      "\"address\":" +
      "{" +
      "\"streetAddress\": \"21 2nd Street\"," +
      "\"city\": \"New York\"," +
      "\"state\": \"NY\"," +
      "\"postalCode\": \"10021-3100\"" +
      "}," +
      "\"phoneNumbers\":" +
      "[" +
      "{" +
      "\"type\": \"home\"," +
      "\"number\": \"212 555-1234\"" +
      "}," +
      "{" +
      "\"type\": \"office\"," +
      "\"number\": \"646 555-4567\"" +
      "}" +
      "]," +
      "\"children\": []," +
      "\"spouse\": null" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println(json);
   }
}

Listing 8-1.Reading and Parsing a String-Based JSON Object

main(String[])方法首先声明一个基于 Java 字符串的 JSON 对象(可以用逗号(,)代替冒号(:),但是冒号更清楚);然后调用Json.read()读取并解析该对象,并将该对象作为Json对象返回;最后输出Json对象的字符串表示(通过最终调用它的toString()方法将Json对象转换成 Java 字符串)。

编译清单 8-1 如下:

javac -cp mjson-1.3.jar mJsonDemo.java

运行生成的应用程序,如下所示:

java -cp mjson-1.3.jar;. mJsonDemo

您应该观察到以下输出:

{"firstName":"John","lastName":"Smith","isAlive":true,"address":{"streetAddress":"21 2nd Street","city":"New York","postalCode":"10021-3100","state":"NY"},"children":[],"age":25,"phoneNumbers":[{"number":"212 555-1234","type":"home"},{"number":"646 555-4567","type":"office"}],"spouse":null}

read()方法也可以解析更小的 JSON 片段,比如不同类型值的数组。参见清单 8-2 进行演示。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      Json json = Json.read("[4, 5, {}, true, null, \"ABC\", 6]");
      System.out.println(json);
   }
}

Listing 8-2.Reading and Parsing a JSON Fragment

当您运行此应用程序时,您应该观察到以下输出:

[4,5,{},true,null,"ABC",6]

除了读取和解析方法,Json还提供了用于创建Json对象的static方法:

  • Json array():返回一个代表空 JSON 数组的Json对象。
  • Json array(Object... args):返回一个填充了argsJson对象(代表一个 JSON 数组),变量为java.lang.Object s
  • Json make(Object anything):返回一个填充了anything内容的Json对象,为null之一;一个类型为JsonStringjava.util.Collection<?>java.util.Map<?, ?>java.lang.Booleanjava.lang.Number的值;或者这些类型之一的数组。映射、集合和数组被递归复制,使得它们的每个元素都被转换成一个Json对象。映射的键通常是字符串,但是任何具有有意义的toString()实现的对象都可以。当传递给anything的参数的具体类型未知时,该方法抛出java.lang.IllegalArgumentException
  • Json nil():返回一个代表nullJson对象。
  • Json object():返回一个代表空 JSON 对象的Json对象。
  • Json object(Object... args):返回一个Json对象(代表一个 JSON 对象)填充args,一个Object的变量数,这些对象标识属性名和值;对象的数量必须是偶数,偶数索引标识属性名,奇数索引标识属性值。这些名字通常是String类型的,但是也可以是具有适当的toString()方法的任何其他类型。每个值首先通过调用 make(Object )转换成一个Json对象。

清单 8-3 展示了一个应用程序的源代码,该应用程序演示了大多数这些额外的static方法。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      Json jsonAddress =
         Json.object("streetAddress", "21 2nd Street",
                     "city", "New York",
                     "state", "NY",
                     "postalCode", "10021-3100");
      Json jsonPhone1 =
         Json.object("type", "home",
                     "number", "212 555-1234");
      Json jsonPhone2 =
         Json.object("type", "office",
                     "number", "646 555-4567");
      Json jsonPerson =
         Json.object("firstName", "John",

                     "lastName", "Smith",
                     "isAlive", true,
                     "age", 25,
                     "address", jsonAddress,
                     "phoneNumbers", Json.array(jsonPhone1, jsonPhone2),
                     "children", Json.array(),
                     "spouse", Json.nil());
      System.out.println(jsonPerson);
   }
}

Listing 8-3.Creating a Person JSON Object

清单 8-3 描述了一个创建与清单 8-1 中读取和解析的 JSON 对象相同的应用程序。注意,您可以将Json对象传递给array(Object...)object(Object...),这允许您从较小的片段构建完整的 JSON 对象。如果您运行这个应用程序,您会发现与清单 8-1 中描述的应用程序生成的输出相同。

清单 8-4 展示了另一个将make(Object)与 Java 集合和映射结合使用的应用程序的源代码。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      List<String> weekdays = Arrays.asList("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday");
      System.out.println(Json.make(weekdays));

      Map<String, Number> people = new HashMap<>();
      people.put("John", 33);
      people.put("Joan", 27);
      System.out.println(Json.make(people));

      Map<String, String[]> planets = new HashMap<>();
      planets.put("Mercury", null);
      planets.put("Earth", new String[] {"Luna"});
      planets.put("Mars", new String[] {"Phobos", "Deimos"});
      System.out.println(Json.make(planets));
   }
}

Listing 8-4.Making JSON Objects from Java Collections and Maps

main(String[])首先创建一个星期几名称的列表,然后将这个对象传递给make(Object),后者返回的Json对象被输出。接下来,人们的姓名和年龄的地图被创建并随后被传递给make(Object)。输出最终的 JSON 对象。最后,创建了一个行星名称和卫星名称数组的地图。这个 map 被转换成一个更复杂的 JSON 对象,并输出。

如果您编译这个源代码并运行应用程序,您会发现以下输出:

["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]
{"Joan":27,"John":33}

{"Earth":["Luna"],"Mars":["Phobos","Deimos"],"Mercury":null}

了解 Json 对象

Json提供了几种学习由Json对象描述的 JSON 实体的方法。首先,您可以调用Object getValue()方法来返回Json对象的 JSON 值(作为 Java 对象)。返回值将是 Java null或具有 Java BooleanStringNumberMapjava.util.List或数组类型。对于对象和数组,此方法执行所有嵌套元素的深层复制。

要标识 JSON 值的 JSON 类型,请调用以下方法之一:

  • boolean isArray():返回 JSON 数组值的true
  • boolean isBoolean():返回一个 JSON 布尔值true
  • boolean isNull():返回 JSON null值的true
  • boolean isNumber():返回 JSON 数值的true
  • boolean isObject():返回 JSON 对象值的true
  • boolean isPrimitive():返回 JSON 数字、字符串或布尔值的true
  • boolean isString():返回 JSON 字符串值的true

清单 8-5 展示了演示getValue()和这些 JSON 类型识别方法的应用程序的源代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"lastName\": \"Smith\"," +
      "\"isAlive\": true," +
      "\"age\": 25," +
      "\"address\":" +
      "{" +
      "\"streetAddress\": \"21 2nd Street\"," +
      "\"city\": \"New York\"," +
      "\"state\": \"NY\"," +
      "\"postalCode\": \"10021-3100\"" +
      "}," +
      "\"phoneNumbers\":" +
      "[" +
      "{" +
      "\"type\": \"home\"," +
      "\"number\": \"212 555-1234\"" +
      "}," +
      "{" +
      "\"type\": \"office\"," +
      "\"number\": \"646 555-4567\"" +
      "}" +
      "]," +
      "\"children\": []," +
      "\"spouse\": null" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println("Value = " + json.getValue());
      System.out.println();
      classify(json);
   }

   static void classify(Json jsonObject)
   {
      if (jsonObject.isArray())
         System.out.println("Array");
      else
      if (jsonObject.isBoolean())
         System.out.println("Boolean");
      else
      if (jsonObject.isNull())
         System.out.println("Null");
      else
      if (jsonObject.isNumber())
         System.out.println("Number");
      else

      if (jsonObject.isObject())
         System.out.println("Object");
      else
      if (jsonObject.isString())
         System.out.println("String");
      if (jsonObject.isPrimitive())
         System.out.println("Primitive");
   }
}

Listing 8-5.Obtaining a Json Object’s Value and Identifying Its JSON Type

编译这段源代码并运行应用程序,您会发现以下输出:

Value = {firstName=John, lastName=Smith, isAlive=true, address={streetAddress=21 2nd Street, city=New York, postalCode=10021-3100, state=NY}, children=[], age=25, phoneNumbers=[{number=212 555-1234, type=home}, {number=646 555-4567, type=office}], spouse=null}

Object

在验证了一个Json对象代表了预期的 JSON 类型之后,您可以调用Json的一个as方法来获取 JSON 值,作为一个等价 Java 类型的 Java 值:

  • boolean asBoolean():以 Java 布尔值的形式返回 JSON 值。
  • byte asByte():以 Java 字节整数的形式返回 JSON 值。
  • char asChar():返回 JSON 字符串值的第一个字符作为 Java 字符。
  • double asDouble():返回 JSON 值作为 Java 双精度浮点值。
  • float asFloat():以 Java 浮点值的形式返回 JSON 值。
  • int asInteger():返回 Java 整数形式的 JSON 值。
  • List<Json> asJsonList():返回 JSON 数组的底层列表表示。返回的列表是实际的数组表示,所以对它的任何修改都是对Json对象列表的修改。
  • Map<String, Json> asJsonMap():返回 JSON 对象属性的底层映射。返回的贴图是实际的对象表示,所以对它的任何修改都是对Json对象贴图的修改。
  • List<Object> asList():返回描述 JSON 数组的Json对象的元素列表。返回的列表是一个副本,对它的修改不会影响Json对象。
  • long asLong():以 Java 长整型返回 JSON 值。
  • Map<String, Object> asMap():返回描述 JSON 对象的Json对象的属性图。返回的地图是副本,对它的修改不会影响Json对象。
  • short asShort():以 Java 短整型返回 JSON 值。
  • String asString():以 Java 字符串的形式返回 JSON 值。

清单 8-6 展示了一个应用程序的源代码,该应用程序使用asMap()来获取描述 JSON 对象的Json对象属性的映射。

import java.util.Map;

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"lastName\": \"Smith\"," +
      "\"isAlive\": true," +
      "\"age\": 25," +
      "\"address\":" +
      "{" +
      "\"streetAddress\": \"21 2nd Street\"," +
      "\"city\": \"New York\"," +
      "\"state\": \"NY\"," +
      "\"postalCode\": \"10021-3100\"" +
      "}," +
      "\"phoneNumbers\":" +
      "[" +
      "{" +
      "\"type\": \"home\"," +
      "\"number\": \"212 555-1234\"" +
      "}," +
      "{" +
      "\"type\": \"office\"," +
      "\"number\": \"646 555-4567\"" +
      "}" +
      "]," +

      "\"children\": []," +
      "\"spouse\": null" +
      "}";
      Json json = Json.read(jsonStr);
      if (json.isObject())
      {
         Map<String, Object> props = json.asMap();
         for (Map.Entry<String, Object> propEntry: props.entrySet())
            System.out.println(propEntry.getKey() + ": " +  propEntry.getValue());
      }
   }
}

Listing 8-6.Iterating Over a Json Object’s Properties to Learn About a JSON Object

main(String[])声明了与清单 8-1 中相同的 JSON 对象。然后它读取这个对象并解析成一个Json对象。调用isObject()方法来验证Json对象表示一个 JSON 对象。(最好先核实一下。)因为应该是这种情况,所以调用asMap()来返回Json对象属性的映射,然后遍历并输出。

Caution

如果用Json json = Json.make(jsonStr);替换Json json = Json.read(jsonStr);,您将看不到任何输出,因为从make()返回的Json对象标识 JSON 字符串类型,而不是 JSON 对象类型。

研究完源代码后,编译它并运行应用程序。您将发现以下输出:

firstName: John
lastName: Smith
isAlive: true
address: {streetAddress=21 2nd Street, city=New York, postalCode=10021-3100, state=NY}
children: []
age: 25
phoneNumbers: [{number=212 555-1234, type=home}, {number=646 555-4567, type=office}]
spouse: null

您可以通过调用以下at()方法来访问数组和对象的内容,这些方法返回描述数组元素值或对象属性值的Json对象:

  • Json at(int index):返回该Json对象数组中指定index处数组元素的值(作为Json对象)。这个方法只适用于 JSON 数组。当index超出数组界限时,它抛出java.lang.IndexOutOfBoundsException
  • Json at(String propName):返回对象属性的值(作为一个Json对象),该对象属性的名称由该Json对象的地图中的propName标识。当没有这样的属性时返回null。这个方法只适用于 JSON 对象。
  • Json at(String propName, Json defValue):返回对象属性的值(作为一个Json对象),该对象属性的名称由该Json对象的地图中的propName标识。当没有这样的属性时,它创建一个新的属性,其值由defValue指定,并返回defValue。这个方法只适用于 JSON 对象。
  • Json at(String propName, Object defValue):返回对象属性的值(作为一个Json对象),该对象属性的名称由该Json对象的地图中的propName标识。当没有这样的属性时,它创建一个新的属性,其值由defValue指定,并返回defValue。这个方法只适用于 JSON 对象。

清单 8-7 展示了使用前两个at()方法访问 JSON 对象属性值的应用程序的源代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"lastName\": \"Smith\"," +
      "\"isAlive\": true," +
      "\"age\": 25," +
      "\"address\":" +
      "{" +
      "\"streetAddress\": \"21 2nd Street\"," +
      "\"city\": \"New York\"," +
      "\"state\": \"NY\"," +
      "\"postalCode\": \"10021-3100\"" +
      "}," +
      "\"phoneNumbers\":" +
      "[" +
      "{" +
      "\"type\": \"home\"," +
      "\"number\": \"212 555-1234\"" +
      "}," +
      "{" +
      "\"type\": \"office\"," +
      "\"number\": \"646 555-4567\"" +
      "}" +
      "]," +
      "\"children\": []," +
      "\"spouse\": null" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.printf("First name = %s%n", json.at("firstName"));
      System.out.printf("Last name = %s%n", json.at("lastName"));

      System.out.printf("Is alive = %s%n", json.at("isAlive"));
      System.out.printf("Age = %d%n", json.at("age").asInteger());
      System.out.println("Address");
      Json jsonAddr = json.at("address");
      System.out.printf("   Street address = %s%n",  jsonAddr.at("streetAddress"));
      System.out.printf("   City = %s%n", jsonAddr.at("city"));
      System.out.printf("   State = %s%n", jsonAddr.at("state"));
      System.out.printf("   Postal code = %s%n", jsonAddr.at("postalCode"));
      System.out.println("Phone Numbers");
      Json jsonPhone = json.at("phoneNumbers");
      System.out.printf("   Type = %s%n", jsonPhone.at(0).at("type"));
      System.out.printf("   Number = %s%n", jsonPhone.at(0).at("number"));
      System.out.println();
      System.out.printf("   Type = %s%n", jsonPhone.at(1).at("type"));
      System.out.printf("   Number = %s%n", jsonPhone.at(1).at("number"));
      Json jsonChildren = json.at("children");
      System.out.printf("Children = %s%n", jsonChildren);
      System.out.printf("Spouse = %s%n", json.at("spouse"));
   }
}

Listing 8-7.Obtaining and Outputting a JSON Object’s Property Values

表达式json.at("age")返回描述 JSON 编号的Json对象;asInteger()以 32 位 Java 整数的形式返回该值。

编译这个源代码并运行应用程序。您将发现以下输出:

First name = "John"
Last name = "Smith"
Is alive = true
Age = 25
Address
   Street address = "21 2nd Street"
   City = "New York"
   State = "NY"
   Postal code = "10021-3100"
Phone Numbers
   Type = "home"
   Number = "212 555-1234"

   Type = "office"
   Number = "646 555-4567"
Children = []
Spouse = null

您可能想知道如何检测分配给属性名children的空数组。您可以通过调用asList()返回一个List实现对象,然后在这个对象上调用Listsize()方法来完成这个任务,如下所示:

System.out.printf("Array length = %d%n", jsonChildren.asList().size());

这段代码将报告一个长度为零的数组。

最后,Json提供了三种方法来验证属性名是否存在,以及属性名或数组元素是否具有指定的值:

  • boolean has(String propName):当这个Json对象描述一个 JSON 对象,这个 JSON 对象有一个由propName标识的属性时,返回true;否则,返回false
  • boolean is(int index, Object value):当这个Json对象描述一个 JSON 数组在指定的index有指定的value时,返回true;否则,返回false
  • boolean is(String propName, Object value):当这个Json对象描述了一个 JSON 对象,这个 JSON 对象有一个由propName标识的属性,并且这个属性有一个由value标识的值时,返回true;否则,返回false

例如,考虑上市 8-7 。表达式json.has("firstName")返回true,而表达式json.has("middleName")返回false

导航 Json 对象层次结构

当前面讨论的at()方法之一返回描述 JSON 对象或 JSON 数组的Json对象时,您可以通过将另一个at()方法调用链接到表达式来导航到该对象或数组。例如,我在前面的应用程序中使用这种技术来访问一个电话号码:

System.out.printf("   Number = %s%n", jsonPhone.at(0).at("number"));

这里,jsonPhone.at(0)返回一个代表phoneNumbers JSON 数组中第一个数组条目的Json对象。因为数组条目恰好是一个 JSON 对象,所以在这个Json对象上调用at("number")会导致Json返回 JSON 对象的number属性的值(作为一个Json对象)。

每个描述属于一个数组或对象的 JSON 实体的Json对象都包含一个对其封闭的基于数组或对象的Json对象的引用。您可以调用JsonJson up()方法来返回这个封闭的Json对象,如清单 8-8 所示。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"propName\": \"propValue\"," +
      "\"propArray\":" +
      "[" +
      "{" +
      "\"element1\": \"value1\"" +
      "}," +
      "{" +
      "\"element2\": \"value2\"" +
      "}" +
      "]" +
      "}";
      Json json = Json.read(jsonStr);
      Json jsonElement1 = json.at("propArray").at(0);
      System.out.println(jsonElement1);
      System.out.println();
      System.out.println(jsonElement1.up());
      System.out.println();
      System.out.println(jsonElement1.up().up());
      System.out.println();
      System.out.println(jsonElement1.up().up().up());
   }
}

Listing 8-8.Accessing Enclosing Json Objects

编译这段源代码并运行应用程序,您会发现以下输出:

{"element1":"value1"}

[{"element1":"value1"},{"element2":"value2"}]

{"propArray":[{"element1":"value1"},{"element2":"value2"}],"propName":"propValue"}

null

第一个输出行描述了数组中分配给propArray属性的第一个数组元素。这个元素是由单个element1属性组成的对象。

jsonElement1.up()返回一个Json对象,描述包含 JSON 对象的数组,该对象作为数组的第一个元素。jsonElement1.up().up()返回一个Json对象,描述包含数组的 JSON 对象。最后,jsonElement1.up().up().up()返回一个描述null值的Json对象;JSON 对象没有父对象。

修改 Json 对象

您会遇到想要修改现有Json对象的 JSON 值的情况。例如,您可能正在创建和保存几个相似的 JSON 对象,并希望重用现有的Json对象。

Json允许您修改代表 JSON 数组和对象的Json对象。它不允许您修改代表 JSON 布尔值、数字或字符串值的Json对象,因为它们被认为是不可变的。

Json声明了以下用于修改 JSON 数组元素和 JSON 对象属性的set()方法:

  • Json set(int index, Object value):将位于index的 JSON 数组元素的值设置为value
  • Json set(String propName, Json value):将名称由propName指定的 JSON 对象属性的值设置为value
  • Json set(String property, Object value):将名称由propName指定的 JSON 对象属性的值设置为value。该方法调用make(Object)value转换为代表valueJson对象,然后调用set(String, Json)

清单 8-9 展示了使用第一个和第三个set()方法来设置对象属性和数组元素值的应用程序的源代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"name\": null," +
      "\"courses\":" +
      "[null]" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println(json);
      System.out.println();
      json.set("name", "John Doe");
      Json jsonCourses = json.at("courses");
      jsonCourses.set(0, "English");
      System.out.println(json);
   }
}

Listing 8-9.Setting Object Property and Array Element Values

如果您编译这个源代码并运行应用程序,您会发现以下输出:

{"courses":[null],"name":null}

{"courses":["English"],"name":"John Doe"}

如果您试图为一个不存在的属性设置一个值,Json会添加该属性。但是,如果试图为一个不存在的数组元素设置值,Json会抛出IndexOutOfBoundsException。因此,您可能更喜欢调用下面的add()方法:

  • Json add(Json element):将指定的元素追加到由这个Json对象表示的数组中。
  • Json add(Object anything):通过调用make(Object)anything转换成一个Json对象,并将结果附加到由这个Json对象表示的数组中。

清单 8-10 展示了一个应用程序的源代码,该应用程序使用第一个add()方法向空的courses数组添加两个字符串。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"name\": null," +
      "\"courses\":" +
      "[]" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println(json);
      System.out.println();
      json.set("name", "John Doe");
      Json jsonCourses = json.at("courses");
      jsonCourses.add("English");
      jsonCourses.add("French");
      System.out.println(json);
   }
}

Listing 8-10.Appending Strings to an Empty JSON Array

编译这个源代码并运行应用程序。它生成如下所示的输出:

{"courses":[],"name":null}

{"courses":["English","French"],"name":"John Doe"}

Json提供了一对面向数组的remove()方法,它们与它们的add()对应方法采用相同的参数:

  • Json remove(Json element):从这个Json对象表示的数组中删除指定的元素。
  • Json remove(Object anything):通过调用make(Object)并将结果从这个Json对象代表的数组中移除,将anything转换成一个Json对象。

假设您将下面几行添加到清单 8-10 的main(String[])方法中:

jsonCourses.remove("English");
System.out.println(json);

然后,您应该观察到以下附加输出:

{"courses":["French"],"name":"John Doe"}

通过调用以下方法,可以按索引从数组中移除元素,或按名称从对象中移除属性:

  • Json atDel(int index):从这个Json对象的 JSON 数组中删除指定index处的元素,并返回该元素。
  • Json atDel(String propName):从这个Json对象的 JSON 对象中删除由propName标识的属性,并返回属性值(如果属性不存在,则返回null)。
  • Json delAt(int index):从这个Json对象的 JSON 数组中删除指定index处的元素。
  • Json delAt(String propName):从这个Json对象的 JSON 对象中删除由propName标识的属性。

清单 8-11 展示了使用最后两个delAt()方法删除属性和数组元素的应用程序的源代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"lastName\": \"Doe\"," +
      "\"courses\":" +
      "[\"English\", \"French\", \"Spanish\"]" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println(json);
      System.out.println();
      json.delAt("lastName");
      System.out.println(json);
      System.out.println();
      json.at("courses").delAt(1);
      System.out.println(json);
   }
}

Listing 8-11.Removing the Last Name and One of the Courses Being Taken

要查看delAt()方法的结果,编译这个源代码并运行应用程序。其输出如下所示:

{"firstName":"John","lastName":"Doe","courses":["English","French","Spanish"]}

{"firstName":"John","courses":["English","French","Spanish"]}

{"firstName":"John","courses":["English","Spanish"]}

Json提供了另一种修改 JSON 对象的方法:

  • Json with(Json objectorarray):将这个Json对象的 JSON 对象或 JSON 数组与传递给objectorarray的参数相结合。这个Json对象的 JSON 类型和objectorarray的 JSON 类型必须匹配。如果objectorarray标识了一个 JSON 对象,那么它的所有属性都被附加到这个Json对象的对象中。如果objectorarray标识了一个 JSON 数组,那么它的所有元素都被附加到这个Json对象的数组中。

清单 8-12 展示了一个应用程序的源代码,该应用程序使用with(Json)将属性添加到一个对象,将元素添加到一个数组。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"courses\":" +
      "[\"English\"]" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println(json);
      System.out.println();
      Json jsono = Json.read("{\"initial\": \"P\", \"lastName\": \"Doe\"}");
      Json jsona = Json.read("[\"French\", \"Spanish\"]");
      json.with(jsono);
      System.out.println(json);
      System.out.println();
      json.at("courses").with(jsona);
      System.out.println(json);
   }
}

Listing 8-12.Appending Properties to an Object and Elements to an Array

编译清单 8-12 并运行应用程序。下面是应用程序的输出:

{"firstName":"John","courses":["English"]}

{"firstName":"John","courses":["English"],"lastName":"Doe","initial":"P"}

{"firstName":"John","courses":["English","French","Spanish"],"lastName":"Doe","initial":"P"}

确认

Json通过其嵌套的Schema接口和以下static方法支持 JSON 模式草案 4 验证:

  • Json.Schema schema(Json jsonSchema):返回一个Json.Schema对象,根据jsonSchema描述的模式验证 JSON 文档。
  • Json.Schema schema(Json jsonSchema, URI uri):返回一个Json.Schema对象,根据jsonSchema描述的模式验证 JSON 文档,也位于传递给uri的统一资源标识符(URI),类型为java.net.URI
  • Json.Schema schema(URI uri):返回一个Json.Schema对象,根据位于uri的模式验证 JSON 文档。

通过调用SchemaJson validate(Json document)方法来执行验证,该方法试图根据这个Schema对象来验证一个 JSON document。即使检测到验证错误,验证也会尝试继续进行。返回值总是一个Json对象,其 JSON 对象包含名为ok的布尔属性。当oktrue时,没有其他属性。当它是false时,JSON 对象还包含一个名为errors的属性,这是所有检测到的模式违规的错误消息数组。

我已经创建了两个演示验证的示例应用程序。清单 8-13 基于 mJson 网站上的示例代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      // A simple schema that accepts only JSON objects with a
         // mandatory property 'id'.
      Json.Schema schema = Json.schema(Json.object("type", "object", "required", Json.array("id")));
      System.out.println(schema.validate(Json.object("id", 666, "name", "Britlan")));

      System.out.println(schema.validate(Json.object("ID", 666, "name", "Britlan")));
   }
}

Listing 8-13.Validating JSON Objects That Include the id Property

如果您编译这个源代码并运行应用程序,您会发现以下输出:

{"ok":true}
{"ok":false,"errors":["Required property id missing from object {\"name\":\"Britlan\",\"ID\":666}"]}

在第七章中,我展示了以下 JSON 对象:

{
   "name": "John Doe",
   "age": 35
}

我还以 JSON 对象的形式展示了以下模式:

{
   "$schema": "http://json-schema.org/draft-04/schema#",
   "title": "Person",
   "description": "A person",
   "type": "object",
   "properties":
   {
      "name":
      {
         "description": "A person's name",
         "type": "string"
      },
      "age":
      {
         "description": "A person's age",
         "type": "number",
         "minimum": 18,
         "maximum": 64
      }
   },
   "required": ["name", "age"]    
}

假设我将这个模式复制到一个schema.json文件中,并将其存储在我的网站上 http://javajeff.ca/schema.json 。清单 8-14 展示了一个应用程序的源代码,该应用程序使用Json.Schema schema(URI)来获取这个模式,以验证之前的 JSON 对象。

import java.net.URI;
import java.net.URISyntaxException;

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args) throws URISyntaxException
   {
      Json.Schema schema =
         Json.schema(new URI("http://javajeff.ca/schema.json"));
      Json json = Json.read("{\"name\": \"John Doe\", \"age\": 35}");
      System.out.println(schema.validate(json));
      json = Json.read("{\"name\": \"John Doe\", \"age\": 65}");
      System.out.println(schema.validate(json));
      json = Json.read("{\"name\": \"John Doe\", \"age\": \"35\"}");
      System.out.println(schema.validate(json));
      json = Json.read("{\"age\": 35}");
      System.out.println(schema.validate(json));
   }
}

Listing 8-14.Validating JSON Objects via an External Schema

编译这个源代码并运行应用程序。您将发现以下输出:

{"ok":true}
{"ok":false,"errors":["Number 65 is above allowed maximum 64.0"]}
{"ok":false,"errors":["Type mistmatch for \"35\", allowed types: [\"number\"]"]}
{"ok":false,"errors":["Required property name missing from object {\"age\":35}"]}

通过工厂定制

JsonJson对象的创建委托给工厂,工厂是实现Json.Factory接口方法的类的实例:

  • Json array()
  • Json bool(boolean value)
  • Json make(Object anything)
  • Json nil()
  • Json number(Number value)
  • Json object()
  • Json string(String value)

Json.DefaultFactory类提供了这些方法的默认实现,但是您可以在必要时提供自定义实现。为了避免实现所有这些方法,您可以扩展 DefaultFactory ,只覆盖那些感兴趣的方法。

创建自定义的Factory类后,实例化它,然后通过调用以下static Json方法之一来安装对象:

  • T0
  • void attachFactory(Json.Factory factory)

第一种方法将指定的factory安装为一个全局工厂,它由所有没有特定线程本地工厂的线程使用。第二种方法仅将指定的factory附加到调用线程,这允许您在同一个类加载器中使用不同的线程工厂。您可以通过调用void dettachFactory()方法移除线程本地工厂,并恢复到线程的全局工厂。

mJson 文档中提到的定制之一是不区分大小写的字符串比较。通过调用基于字符串的Json对象上的equals()和另一个基于字符串的Json对象作为参数,比较两个字符串是否相等:

Json json1 = Json.read("\"abc\"");
Json json2 = Json.read("\"abc\"");
Json json3 = Json.read("\"Abc\"");
System.out.println(json1.equals(json2)); // Output: true
System.out.println(json1.equals(json3));

因为equals()默认区分大小写,json1.equals(json3)返回false

Note

被调用的equals()方法不在Json类中。相反,它位于一个嵌套的包私有类中,比如StringJson

通过首先创建下面的Factory类,可以使基于字符串的Json对象的equals()不区分大小写:

class MyFactory extends Json.DefaultFactory
{
   @Override
   public Json string(String x)
   {

      // Obtain the StringJson instance.
      final Json json = super.string(x);

      class StringIJson extends Json
      {
         private static final long serialVersionUID = 1L;

         String val;

         StringIJson(String val)
         {
            this.val = val;
         }

         @Override
         public byte asByte()
         {
            return json.asByte();
         }

         @Override
         public char asChar()
         {
            return json.asChar();
         }

         @Override
         public double asDouble()
         {
            return json.asDouble();
         }

         @Override
         public float asFloat()
         {
            return json.asFloat();
         }

         @Override
         public int asInteger()
         {
            return json.asInteger();
         }

         @Override
         public List<Object> asList()
         {
            return json.asList();

         }

         @Override
         public long asLong()
         {
            return json.asLong();
         }

         @Override
         public short asShort()
         {
            return json.asShort();
         }

         @Override
         public String asString()
         {
            return json.asString();
         }

         @Override
         public Json dup()
         {
            return json.dup();
         }

         @Override
         public boolean equals(Object x)
         {
            return x instanceof StringIJson &&
                   ((StringIJson) x).val.equalsIgnoreCase(val);
         }

         @Override
         public Object getValue()
         {
            return json.getValue();
         }

         @Override
         public int hashCode()
         {
            return json.hashCode();
         }

         @Override

         public boolean isString()
         {
            return json.isString();
         }

         @Override
         public String toString()
         {
            return json.toString();
         }
      }
      return new StringIJson(x);
   }
}

MyFactory覆盖了string(String)方法,该方法负责创建代表 JSON 字符串的Json对象。在Json.java源代码中(可以从 mJson 网站获得),string(String)执行return new StringJson(x, null);

StringJson是嵌套的包私有static类的名称。因为不能从mjson包外部访问,MyFactory的覆盖string(String)方法声明了一个等价的StringIJson类(I不区分大小写)。

我选择使用适配器/包装器设计模式( https://en.wikipedia.org/wiki/Adapter_pattern ),而不是将所有代码从StringJson复制到StringIJson,这是浪费的重复,而且无论如何也不会工作,因为一些代码依赖于其他包私有类型。

适配器模式背后的想法是让StringIJson复制StringJson方法的头部,并编码主体将几乎所有的方法调用转发给StringJson的对等物。这可以通过让MyFactorystring(String)方法首先调用DefaultFactorystring(String)方法来实现,后者返回StringJson对象。然后,将调用转移到这个对象就很简单了。

例外是equals()方法。StringIJson将该方法整理成与它的StringJson对应方法几乎相同。主要区别是对StringequalsIgnoreCase()方法的调用,而不是对其equals()方法的调用。结果是一个不区分大小写的equals()方法。

在执行任何相等性测试之前,MyFactory需要被实例化并向Json注册,这由下面的方法调用完成:

Json.setGlobalFactory(new MyFactory());

这次,json1.equals(json3)返回true

Exercises

以下练习旨在测试你对第八章内容的理解。

Define mJson.   Describe the Json class.   Identify Json’s methods for reading and parsing external JSON objects.   True or false: The read() methods can also parse smaller JSON fragments, such as an array of different-typed values.   Identify the methods that Json provides for creating JSON objects.   What does Json’s boolean isPrimitive() method accomplish?   How do you return a Json object’s JSON array?   True or false: Json’s Map<String, Json> asJsonMap() method returns a map of the properties of a Json object that describes a JSON object. The returned map is a copy and modifications to it don’t affect the Json object.   Which Json methods let you access the contents of arrays and objects?   What does Json’s boolean is(int index, Object value) method accomplish?   What does Json do when you attempt to set the value for a nonexistent array element?   What is the difference between Json’s atDel() and delAt() methods?   What does Json’s Json with(Json objectorarray) method accomplish?   Identify Json’s methods for obtaining a Json.Schema object.   How do you validate a JSON document against a schema?   What is the difference between Json’s setGlobalFactory() and attachFactory() methods?   Two Json methods that were not discussed in this chapter are Json dup() and String pad(String callback). What do they do?   Write an mJsonDemo application that demonstrates dup() and pad().  

摘要

mJson 是一个基于 Java 的小型 Json 库,用于将 JSON 对象解析成 Java 对象,反之亦然。它由一个描述 JSON 对象或 JSON 对象的一部分的Json类组成。Json包含SchemaFactory接口,50 多个方法,以及其他成员。

获得 mJson 库之后,您学习了如何使用这个库来创建Json对象,了解了Json对象,导航了Json对象层次结构,修改了Json对象,根据模式验证了 Json 文档,并通过安装非默认工厂定制了Json

第九章介绍了用于解析和创建 JSON 对象的 Gson。