现在的位置: 首页 > 综合 > 正文

使用 StAX 解析 XML,第 3 部分: 使用定制事件和编写 XML

2013年01月31日 ⁄ 综合 ⁄ 共 15977字 ⁄ 字号 评论关闭

定义定制事件并使用 StAX 的序列化器 API

Peter Nehrer (pnehrer@ecliptical.ca),
自由撰稿人, 独立咨询顾问

简介: 除了提供一个低层的基于指针的 API 之外,StAX 还提供了一个功能强大的基于迭代器的方法,它通过使用事件对象传送关于解析流的信息,以处理 XML。本系列的第 2 部分 详细研究了这种 API 并提供了一些使用它的例子。在本篇文章中,将介绍定制化技术,该技术使用由应用程序定义的事件,您还将看到如何创建定制事件类并使用它们结合基于事件迭代器的
API 来处理 XML。最后(同样也是重要的一点),将回顾由 StAX 提供的可将 XML 编写为标记流和事件对象的序列化器 API。

查看本系列更多内容

发布日期: 2007 年 7 月 05 日 
级别: 中级 
访问情况 : 2820 次浏览 
评论: 0 (查看 | 添加评论 -
登录)

平均分 4 星 共 1 个评分 平均分
(1个评分)
为本文评分

创建定制事件

当开发复杂应用程序时,使用分层方法(应用程序的下层为其上层提供必需的抽象)构建应用程序通常会很有用。例如,您可能会将处理 XML 的所有代码集合到一个应用程序提供的高层对象模型。这种技术不仅考虑到通用概念和解决方案的重用,并且还可以加速开发时间和产生更易维护的代码。

因为 StAX 所使用的基于拉的方法将应用程序置于解析过程的控制之下,所以您可以将解析过的事件转换为特定于应用程序的模型对象(例如私有消息或者其他结构的构建块)。然而,您可能发现继续使用事件会更为方便,这样,您只需在 XML 内容中简单地创建定制事件就可以表示更复杂的结构。通过在底层 XML 数据结构之上叠加定制类型,就可以简化应用程序代码的开发同时允许低层仍然将这些类型作为事件对象处理(例如,将它们作为事件写入一个输出流)。

StAX 事件层次结构是可扩充的。可以扩展已有的事件和定义全新的事件类型。因为事件对象被定义为 Java™ 接口而不是类,因此您可以自由决定如何实现它们。例如,您可以将已有的对象模型分类并且将每一个类型表示为一个事件。您同样可以通过组装、委托等达到相同的目的。清单 1 显示了一个扩展 Characters 事件来表示特定的数据类型(本例中是一个 Java Date)的值的例子。所有的子类将文本数据转换为数据值。注意,由于标准事件接口的公共实现不提供
StAX,您可以使用 Decorator 模式包装一个已有的Characters 实例并向其委托所有的方法调用。

清单 1. 定制 Characters 事件的扩展来表示 Date 值

                
final DatatypeFactory tf = DatatypeFactory.newInstance();

class DateTime implements Characters {

  private final Characters d;
  private final Date value;

  DateTime(Characters d) {
    this.d = d;
    XMLGregorianCalendar cal = tf.newXMLGregorianCalendar(d.getData());
    value = cal.toGregorianCalendar().getTime();
  }

  Date getValue() { return value; }
  public Characters asCharacters() { return d.asCharacters(); }
  public EndElement asEndElement() { return d.asEndElement(); }
  public StartElement asStartElement() { return d.asStartElement(); }
  public String getData() { return d.getData(); }
  public int getEventType() { return d.getEventType(); }
  public Location getLocation() { return d.getLocation(); }
  public QName getSchemaType() { return d.getSchemaType(); }
  public boolean isAttribute() { return d.isAttribute(); }
  public boolean isCData() { return d.isCData(); }
  public boolean isCharacters() { return d.isCharacters(); }
  public boolean isEndDocument() { return d.isEndDocument(); }
  public boolean isEndElement() { return d.isEndElement(); }
  public boolean isEntityReference() { return d.isEntityReference(); }
  public boolean isIgnorableWhiteSpace() { return d.isIgnorableWhiteSpace(); }
  public boolean isNamespace() { return d.isNamespace(); }
  public boolean isProcessingInstruction() { return d.isProcessingInstruction(); 
}
  public boolean isStartDocument() { return d.isStartDocument(); }
  public boolean isStartElement() { return d.isStartElement(); }
  public boolean isWhiteSpace() { return d.isWhiteSpace(); }
  public void writeAsEncodedUnicode(Writer writer)
      throws XMLStreamException {
    d.writeAsEncodedUnicode(writer);
  }
}

通常新的事件被完全用来表示特定于应用程序的 XML 结构。例如,通过一个定制事件类给出某些标记及其属性和/或文本内容,这个类为访问这种数据定义了一个 API(因此就使应用程序从必须使用 XML API 获取数据中解放出来)。即使一个定制事件由几个标准事件复合而成(如 StartElementCharacters 和 EndElement),它仍然是一个单独类型的事件,因为它不能被映射到这些事件中的任何一个。

XMLEvent 接口

任何定制事件类必须实现 XMLEvent 接口。在 StAX 的当前版本,没有可以通过扩展实现定制事件的抽象基类。幸运的是,您可以在XMLEvent 中很轻松地实现大部分的方法。例如,向下转型方法(asStartElement()asEndElement() 和 asCharacters())中的每一个都可以轻易地抛出一个 ClassCastException(因为这个定制事件不是其中的任何一个)。同样地,布尔查询方法,用于快速决定事件类型(isAttribute()isCharacters() 等),可以仅返回
false。如果一个定制事件由其他事件复合而成,getLocation() 方法可仅返回最后一个事件的位置。getSchemaType() 方法可以从相关联的 XML 模式返回外部事件的复合类型的名称。可以编写writeAsEncodedUnicode(Writer) 方法来依次委派给每一个所包含的事件(例如,对于一个只有简单内容的代表所有元素的定制事件,首先要编写 StartElement,然后是 Characters,接着是 EndElement)。最后,getEventType() 必须返回一个在整个应用程序中表示新事件类型的惟一值。0
到 256 的值被保留下来供 StAX 提供者使用,所以您必须选择这个范围以外的值(并很好地加以说明,这样就不会与整个应用程序中使用的其他定制事件起冲突)。

清单 2 显示了一个定制事件例子,它展现一个 Atom 提要中的图标元素。这个元素由 Atom Syndication Format 规范(1.0 版本)定义,包含了提要的图标的 URL。

清单 2. 展现 Atom 提要图标(只含文本的元素)的定制事件

                
class Icon implements XMLEvent {

  private final StartElement startElement;
  private final String url;
  private final EndElement endElement;

  Icon(StartElement startElement, String url, EndElement endElement) {
    this.startElement = startElement;
    this.url = url;
    this.endElement = endElement;
  }

  String getURL() { return url; }
  public Characters asCharacters() { throw new ClassCastException(); }
  public EndElement asEndElement() { throw new ClassCastException(); }
  public StartElement asStartElement() { throw new ClassCastException(); }
  public int getEventType() { return 257; }
  public Location getLocation() { return endElement.getLocation(); }
  public QName getSchemaType() { return null; }
  public boolean isAttribute() { return false; }
  public boolean isCharacters() { return false; }
  public boolean isEndDocument() { return false; }
  public boolean isEndElement() { return false; }
  public boolean isEntityReference() { return false; }
  public boolean isNamespace() { return false; }
  public boolean isProcessingInstruction() { return false; }
  public boolean isStartDocument() { return false; }
  public boolean isStartElement() { return false; }
  public void writeAsEncodedUnicode(Writer writer)
      throws XMLStreamException {
    startElement.writeAsEncodedUnicode(writer);
    ef.createCharacters(url).writeAsEncodedUnicode(writer);
    endElement.writeAsEncodedUnicode(writer);
  }
}

到目前为止,您已经了解了如何定义定制事件类,但是还不清楚如何让 StAX 在解析过程中使用它们。接下来的章节介绍将定制事件类插入框架中的技术。

使用定制事件解析 XML

定义一个定制事件后,就可以在事件处理代码中使用它。您可以随意将任意的标准事件序列转化为定制事件(并将它传送给充当事件消费者的其他组件),与将事件转换为一个特定于应用程序对象模型所做的一样。但 StAX 为将定制事件实现插入到框架中提供了一个更通用的机制。

XMLEventAllocator API

正如您已了解的一样,在逻辑上基于事件迭代器的 API 是基于指针的 API 上面的一层。事实上,XMLEventReader 的一个标准实现可以包装一个 XMLStreamReader 并在其当前状态的基础上创建事件对象。为了支持应用程序定义的事件,当需要创建一个具体事件时,XMLEventReader 使用一个应用程序提供的 XMLEventAllocator 实例,这可以通过 XMLInputFactory 进行设置。要使用一个定制XMLEventAllocator,应用程序应该通过调用 XMLInputFactory 的 setEventAllocator(XMLEventAllocator) 方法提供它的一个实例。

XMLEventAllocator 本质上是基于指针的 API 和基于事件迭代器的 API 之间的一座桥梁。除了充当其自身的工厂(XMLInputFactory为每一个新的 XMLEventReader 实例调用分配器的 newInstance() 方法)之外,该接口还定义了两个事件分配方法。要返回一个表示
stream reader 当前状态的 XMLEvent 需要使用 allocate(XMLStreamReader)方法,但该方法不能修改 stream reader 的当前状态。这个方法应该主要实现一个指针到事件对象的映射。另一个方法 allocate(XMLStreamReader,
XMLEventConsumer)
 应使用所提供的 stream reader 来创建一个或更多事件对象并将它们传递给所提供的事件消费者。应用程序在处理过程中可以随意修改 stream reader 的状态;例如,它可以将一个单独的状态分成多个事件对象,或相反地,将几个状态聚结为一个事件(比如通过读取一串标记并将它表示为一个事件对象)。

XMLEventAllocator 的使用是可选的,它没有默认的实例(就是说,默认情况下 XMLEventReader 使用创建事件对象的特定于实现的方法),这样有一些不方便,因为它意味着开发人员不能够扩展已有的分配器(比如通过装饰此分配器)而必须从头开始构建自己的分配器。不过,定制的 XMLEventAllocator 不需要提供每个标准事件类型自己的实现方法。它可以(甚至被鼓励这样做)使用XMLEventFactory 来实现此目的。

清单 3 显示了一个简单分配器的实现,它将 Atom 提要元素 “published” 和 “updated” 的文本内容转换为一个 DateTime 事件(Characters 事件的扩展)。这些元素只包含文本内容,其文本应该代表一个日期/时间值。这种定制化由allocate(XMLStreamReader,
XMLEventConsumer)
 方法执行;当遇到合适的 StartElement 时,所有元素的文本(一直到对应的EndElement)都会被转换成单一的 Characters 事件,该事件反过来又由定制 DateTime 事件装饰。结果被馈送给事件消费者。

清单 3. 从元素的文本(其内容代表一个日期/事件值)创建 DateTime 事件的简单事件分配器

                
final String ATOM_NS = "http://www.w3.org/2005/Atom";
final QName PUBLISHED = new QName(ATOM_NS, "published");
final QName UPDATED = new QName(ATOM_NS, "updated");

final XMLEventFactory ef = XMLEventFactory.newInstance();

class CustomAllocator implements XMLEventAllocator {
	
  public XMLEvent allocate(XMLStreamReader r)
      throws XMLStreamException {
    switch (r.getEventType()) {
    case XMLStreamConstants.CDATA:
      return ef.createCData(r.getText());
    case XMLStreamConstants.CHARACTERS:
      if (r.isWhiteSpace())
        return ef.createSpace(r.getText());

      return ef.createCharacters(r.getText());
    case XMLStreamConstants.COMMENT:
      return ef.createComment(r.getText());
    case XMLStreamConstants.DTD:
      return ef.createDTD(r.getText());
    case XMLStreamConstants.END_DOCUMENT:
      return ef.createEndDocument();
    case XMLStreamConstants.END_ELEMENT:
      return ef.createEndElement(r.getName(), allocateNamespaces(r));
    case XMLStreamConstants.PROCESSING_INSTRUCTION:
      return ef.createProcessingInstruction(r.getPITarget(), r.getPIData());
    case XMLStreamConstants.SPACE:
      return ef.createIgnorableSpace(r.getText());
    case XMLStreamConstants.START_DOCUMENT:
      String encoding = r.getEncoding();
      String version = r.getVersion();
      if (encoding != null && version != null && 
r.standaloneSet())
        return ef.createStartDocument(encoding, version, r.isStandalone());

      if (encoding != null && version != null)
        return ef.createStartDocument(encoding, version);

      if (encoding != null)
        return ef.createStartDocument(encoding);

      return ef.createStartDocument();
    case XMLStreamConstants.START_ELEMENT:
      return ef.createStartElement(r.getName(), allocateAttributes(r), 
allocateNamespaces(r));
    default:
      return null;
    }
  }

  private Iterator allocateNamespaces(XMLStreamReader reader) {
    ArrayList namespaces = new ArrayList();
    for (int i = 0, n = reader.getNamespaceCount(); i < n; ++i) {
      Namespace namespace;
      String prefix = reader.getNamespacePrefix(i);
      String uri = reader.getNamespaceURI(i);
      if (prefix == null)
        namespace = ef.createNamespace(uri);
      else
        namespace = ef.createNamespace(prefix, uri);

      namespaces.add(namespace);
    }

    return namespaces.iterator();
  }

  private Iterator allocateAttributes(XMLStreamReader r) {
    ArrayList attributes = new ArrayList();
    for (int i = 0, n = r.getAttributeCount(); i < n; ++i)
      attributes.add(ef.createAttribute(r.getAttributeName(i), 
r.getAttributeValue(i)));

    return attributes.iterator();
  }

  public void allocate(XMLStreamReader reader,
      XMLEventConsumer consumer) throws XMLStreamException {
    XMLEvent event = allocate(reader);
    if (event != null) {
      consumer.add(event);
      if (event.isStartElement()) {
        QName name = event.asStartElement().getName();
        if (PUBLISHED.equals(name) || UPDATED.equals(name)) {
          String text = reader.getElementText();
          Characters delegate = ef.createCharacters(text);
          DateTime dateTime = new DateTime(delegate);
          consumer.add(dateTime);
        }
      }
    }
  }

  public XMLEventAllocator newInstance() {
    return new CustomAllocator();
  }
};

当输入工厂通过定制事件分配器建好之后,从其中创建的每一个事件读取器都将使用它来创建事件对象。应用程序可以像平常一样使用事件读取器,但这些定制事件应能出现在解析过的事件流中(参见清单 4)。

清单 4. 使用定制分配器迭代所创建的事件

                
XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setEventAllocator(new CustomAllocator());
XMLEventReader r = factory.createXMLEventReader(uri, input);

try {
  while (r.hasNext()) {
    XMLEvent event = r.nextEvent();
    if (event instanceof DateTime)
      System.out.println(((DateTime) event).getValue());
  }
} finally {
  r.close();
}

除了可以为 XML 内容创建特定于应用程序的抽象,此方法还可以以更为节省内存的方式使用事件对象。默认情况下,XMLEventReader 将为它返回的每个事件创建一个新的实例。虽然对应用程序来说这可能比较方便 —— 它可以缓存事件,甚至在解析完成之后还可以使用它们 —— 但在资源受限制的环境中或者处理大型 XML 文档时,它可能会带来负面的性能影响。对于不缓存事件对象的应用程序来说,重用每个事件类型的单个实例而不是每次创建一个新的实例(这样很可能会导致频繁的垃圾收集)可能更有效。

在第一次需要它的时候,这样一个静态的分配器可能会创建事件的每个类型,以后每次只返回由新的信息更新过的相同实例。但应用程序却必须了解策略的变化并且相应地处理事件。例如,它必须要十分小心不能顺序拉出两个事件,也不能窥视事件,因为如果这样将丢失先前返回的事件数据。

以 StAX 方式编写 XML

如果不讨论 StAX 的序列化支持,任何对 StAX 的介绍都是不完整的。与输入端一样,输出 XML 也有两种 API。第一种是较低层次的 API,StAX 支持将 XML 编写为 底层的标记流而不需要确保输出是格式良好的(就是说,由应用程序负责在正确的时间调用正确的方法)。另一种较高层次的 API 支持将全部事件对象加到输出流中。

不管何种 API,应用程序都必须首先使用 XMLOutputFactory 创建适当的编写器对象 —— 使用低层 API 的 XMLStreamWriter 或使用基于事件对象 API 的 XMLEventWriter 。要获得默认的 XMLOutputFactory,调用它的静态 getInstance() 方法(就像任何其他的
JAXP 工厂)。

XMLOutputFactory 支持几种类型的输出,这些输出实际上都是与 XMLInputFactory 支持的输入类型相对应的;除了标准的 Java I/OOutputStream 和 Writer,同样也支持
JAXP Result

在 XMLStreamWriter 接口定义的方法和标准的 Java I/O DataOuputStream 中的方法大体相似,只不过前者针对 XML,后者针对的是简单 Java 数据类型。虽然这个编写器不会检查它的输入是否格式良好,但它的确能够确保 CHARACTERS 事件的数据和属性值被正确转义。

要开始编写一个 XML 文档,应用程序首先调用 writeStartDocument 方法的几种重载版本中的一种。其中一种版本具备显式的文档编码和 XML 版本,另一种只有编码,还有一种对编码和 XML 版本(分别为 UTF-8 和 1.0)使用默认值而不设置参数。对于具有文档类型声明(Document Type Declaration,DTD)的文档,应用程序可以调用 writeDTD(String) 并以整个
DTD production 为参数。

writeStartElement 方法的三个版本都可用于编写元素的开始标记。其中一个版本包括前缀、本地名称和名称空间 URI,此版本将前缀绑定到元素的开始标记创建的新的上下文中的名称空间。另一个版本只包含名称空间 URI 和本地名称;这个版本编写没有前缀的元素(假设此名称空间是上下文默认的名称空间)。最后一个版本只包含本地名称,该版本无论默认名称空间如何都会在其内启动元素。
注意在上述所有情况下,都由用户负责写出名称空间属性(就是说,将一个前缀绑定到 URI 并不写出这个名称空间声明)。

除了这些方法,还有三个对应的 writeEmptyElement 方法用来编写空元素(就是说,不具备单独的关闭标记)。这些方法同时打开和关闭元素。

当编写 XML 元素时,应用程序负责管理它们的名称空间。StAX 为此提供了几种方法。您可以分别使用setNamespaceContext(NamespaceContext) 和 getNamespaceContext() 设置并检索全部的名称空间上下文。注意设置一个名称空间的上下文不会写出名称空间声明
—— 它仅定义前缀绑定。这些在代码写 START_ELEMENT 事件时会被使用到。setPrefix(String, String) 方法可以在给定的上下文中将一个前缀绑定到名称空间 URI,getPrefix(String) 检索绑定到给定的
URI 的前缀。setDefaultNamespace(String) 可以在当前的上下文中设置默认的名称空间。

为了准确地写出一个名称空间声明,应用程序可以用一个前缀和一个 URI 调用 writeNamespace(String, String),或者调用writeDefaultNamespace(String) 在当前元素上声明默认名称空间。

编写属性类似于编写元素开始标记,只是您必须同时提供属性值。三个 writeAttribute 方法具有与writeStartElement 方法类似的签名(除了为属性值增加的 String 参数)并且就名称空间而言遵循类似的规则。

除非您使用 writeEmptyElement 方法中的一种,否则为了关闭每个元素的开始标记,在它的内容写出后必须调用writeEndElement()

您可以使用 writeCharacters 和 writeCData 方法编写文本内容。后者可以将文本包装在 CDATA 块中。为了更好地使用内存,还提供了 writeCharacters 的一个版本,它包含一个带有偏移量和长度(而不是 String 对象)的
char 缓冲器。

writeEntityRefwriteProcessingInstruction 和 writeComment 方法可以分别编写实体引用、过程说明和注释。可调用writeEndDocument() 方法标记文档的末尾。

典型的类似流的输出方法,flush 和 close ,可以分别用来促使编写器将任何缓存的数据写入底层输出和关闭编写器对象。注意关闭方法只释放编写器需要的资源,并不关闭底层的输出。

清单 5. 使用 XMLStreamWriter 编写一个简单的 XHTML 文档

                
final String XHTML_NS = "http://www.w3.org/1999/xhtml";
XMLOutputFactory f = XMLOutputFactory.newInstance();
XMLStreamWriter w = f.createXMLStreamWriter(System.out);
try {
      w.writeStartDocument();
      w.writeCharacters("\n");
      w.writeDTD("<!DOCTYPE html " +
                  "PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" " +
                  "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");
      w.writeCharacters("\n");
      w.writeStartElement(XHTML_NS, "html");
      w.writeDefaultNamespace(XHTML_NS);
      w.writeAttribute("lang", "en");
      w.writeCharacters("\n");
      w.writeStartElement(XHTML_NS, "head");
      w.writeStartElement(XHTML_NS, "title");
      w.writeCharacters("Test");
      w.writeEndElement();
      w.writeEndElement();
      w.writeCharacters("\n");
      w.writeStartElement(XHTML_NS, "body");
      w.writeCharacters("This is a test.");
      w.writeEndElement();
      w.writeEndElement();
      w.writeEndDocument();
} finally {
      w.close();
}

与低层 API 相比,XMLEventWriter 使用的基于事件对象的序列化器 API 由更少的方法组成。与其对应的输入端 XMLEventReader 一样,这个编写器使用事件对象表示底层 XML InfoSet 的各个部分。典型的应用程序只需使用 add(XMLEvent) 方法就可写出整个文档。add(XMLEventReader) 是一个很方便的方法,用它可以写出从读取器获得的全部事件。使用这个方法,应用程序可以将整个
XML 流的内容有效地输送到另一个 XML 流,而且内容保持不变。

管理名称空间的一组方法和 XMLStreamWriter 中定义的方法是等价的。getNamespaceContext() 和setNamespaceContext(NamespaceContext) 方法提供了对整个名称空间上下文的访问。setPrefix(String,
String)
 和getPrefix(String) 可以分别绑定和获得名称空间的前缀绑定。最后,setDefaultNamespace(String) 方法可以为当前的名称空间上下文设置默认的名称空间。

正如 XMLStreamWriter 一样,该编写器提供了 flush() 和 close() 方法,可分别用于将缓存数据放到底层输出和关闭编写器。

清单 6. 使用 XMLEventWriter 编写一个简单的 XHTML 文档

                
final String XHTML_NS = "http://www.w3.org/1999/xhtml";
final QName HTML_TAG = new QName(XHTML_NS, "html");
final QName HEAD_TAG = new QName(XHTML_NS, "head");
final QName TITLE_TAG = new QName(XHTML_NS, "title");
final QName BODY_TAG = new QName(XHTML_NS, "body");

XMLOutputFactory f = XMLOutputFactory.newInstance();
XMLEventWriter w = f.createXMLEventWriter(System.out);
XMLEventFactory ef = XMLEventFactory.newInstance();
try {
      w.add(ef.createStartDocument());
      w.add(ef.createIgnorableSpace("\n"));
      w.add(ef.createDTD("<!DOCTYPE html " +
                  "PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" " +
                  "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"));
      w.add(ef.createIgnorableSpace("\n"));
      w.add(ef.createStartElement(HTML_TAG, null, null));
      w.add(ef.createNamespace(XHTML_NS));
      w.add(ef.createAttribute("lang", "en"));
      w.add(ef.createIgnorableSpace("\n"));
      w.add(ef.createStartElement(HEAD_TAG, null, null));
      w.add(ef.createStartElement(TITLE_TAG, null, null));
      w.add(ef.createCharacters("Test"));
      w.add(ef.createEndElement(TITLE_TAG, null));
      w.add(ef.createEndElement(HEAD_TAG, null));
      w.add(ef.createIgnorableSpace("\n"));
      w.add(ef.createStartElement(BODY_TAG, null, null));
      w.add(ef.createCharacters("This is a test."));
      w.add(ef.createEndElement(BODY_TAG, null));
      w.add(ef.createEndElement(HTML_TAG, null));
      w.add(ef.createEndDocument());
} finally {
      w.close();
}

结束语

本系列向您介绍了 StAX 以及如何在一些需要读/写(或两者都需要)XML 的应用程序中使用它。将 XML 流表示为一系列事件对象的做法功能十分强大,可同时提供灵活性和高效性。StAX 将成为 Java Standard Edition 下一版本的一部分,每个 Java 开发人员都会最终使用到它。

参考资料

学习

获得产品和技术

抱歉!评论已关闭.