XML Java核心技術 讀書筆記

XML文檔的結構

XML文檔應當以一個文檔頭開始,如:

<?xml version="1.0"?>

<?xml version="1.0" encoding="UTF-8"?>

嚴格說來,文檔頭是可有可無的,但是強烈推薦使用文檔頭。

文檔頭之後通常是文檔類型定義(Document Type Definition,DTD),如:

<!DOCTYPE web-app PUBLIC"-//Sun Microsystems,Inc.//DTD Web Application 2.2//EN""http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">   

文檔類型定義是確保文檔正確的一個重要機制,但是這不是必需的。

最後,XML文檔的正文包含根元素根元素包含其他一些元素。如:

<?xml version="1.0"?>
<!DOCTYPE configuration ...>
<configuration>
	<title>
		<font>
			<name>Helvetica</name>
			<size>36</size>
		</font>
	</title>
	...
</configuration>
注意:在設計XML文檔結構時,最好使元素只包含子元素或只包含文本,也就是應該避免以下情況 :

<font>

Helvetica

<size>36</size>

</font>

在XML規範中,這叫混合式內容。如果避免了混合內容,可以簡化解析過程。


XML元素可以包含屬性,如<size unit="pt">36</size>

屬性的靈活性比元素差,關於使用元素或屬性的一個通常經驗法則是,屬性只應該在修改值的解釋時使用,而不是在指定值時使用。

注意:在HTML中屬性的使用規則很簡:凡是不顯示在網頁上的都是屬性,如<a href="http://java.sum.com">Java Technology</a>;然而,這個規則對於大多數XML並不那麼管用。因爲XML文件的數據並非像通常意義那樣是讓人瀏覽的。元素和文本是XML文檔的主要要素,以下是你會遇到的其他一些標記的說明:

  • 字符引用的形式是&#十進制值或&#x十六進制值。
  • 實例引用的形式是&name。以下實例引用 

&lt;

&gt;

&amp;

&quot;

&apos;

它們表示:小於,大於 ,&,引號,省略號等字符。可以在DTD中定義其他的實體引用。

  • CDATA部分用<![  和 ]]>來限定界限。它們是字符數據的一種特殊形式。你可以使用它們來包含那些含有<,>,&之類字符的字符串,而不必將它們解釋爲標記,如:<![ CDATA[  <&> are my favorite delimiters  ] ]>,CDATA部分不能包含字符呂]]>。它常被用做將傳統數據納入XML文檔的一種特殊方法。
  • 處理指令是指那些專門在處理XML文檔的應用程序中使用的指令,它們將用<?   和   ?>來限定其界限,例如:<?xml-stylesheet href="mystyle.css" type="text/css"?>
每個XML都以下一個處理指令開頭:<?xml version="1.0"?>

  • 註釋用 <!-  和  -->限定其界限,例如:<!-- This is a comment.  -->註釋不能含有字符串--。註釋只是爲了給文檔的讀者提供信息,其中絕不含有隱藏的命令,命令是由處理指令來實現。

解析XML文檔

Java庫提供了兩個XML解析器:

像文檔對象模型(Document Object Model,DOM)解析器這樣的樹型解析器,它們將讀入的XML文檔轉換成樹結構。

像用於XML的簡單API(Simple API for XML,SAX)解析器這樣的流機制解析器,它們在讀入XML文檔時生成相應的事件。

DOM解析器對於實現我們的大多數目的都很容易。但如果處理很長的文檔,使用它生成樹結構將會消耗大量內存,或者如果你只是對於某些元素感興趣,而不關心它們的上下文,那麼你應該考慮使用流機制解析器。

DOM解析器的接口已經被W3C標準化了。Java中org.w3c.dom包中包含了接口類型的定義,如Document和Element等。可通過以下代碼來獲得Document:

			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder buider = factory.newDocumentBuilder();
			File f = new File("...xml");
			Document doc = buider.parse(f);


通過GetDocumentElement方法將返回文檔根元素:

Element root = doc.getDocumentElement();

getChildNodes方法將返回一個類型爲NodeList的集合,包含了所有的子元素。其中item方法將得到指定索引項,getLength方法則提供項的總數,以下代碼枚舉所有子元素:

			NodeList children = root.getChildNodes();
			for(int i = 0; i < children.getLength(); i++){
				Node child = children.item(i);
				...
			}

注意:分析子元素要很仔細。如下面文檔 :

		<font>
			<name>Helvetica</name>
			<size>36</size>
		</font>

你期望font有兩個子元素,但解析器卻報告有5個:

<font>和<name>之間的空白字符

name元素

</name>和<size>之間的空白字符

size元素

</size>和</font>之間的空白字符


如果只希望得到子元素,可以通過以下代碼忽略空白字符:

			NodeList children = root.getChildNodes();
			for(int i = 0; i < children.getLength(); i++){
				Node child = children.item(i);
				if(child instanceof Element){
					Element childElement = (Element)child;
					...
				}
			}

如果你的文檔在有DTD(下面會講到),那麼解析器會知道哪些元素沒有文本節點子元素,而且它會幫你禁止空白字符。

也可以通過getFirstChild得到第一個子元素,用getNextSiblingt得到下一個兄弟節點,可用以下代碼遍歷子節點:

			for(Node chiNode = root.getFirstChild(); chiNode != null; chiNode = chiNode.getNextSibling()){
				...
			}


當你分析name和size元素時,想檢索到它們包含的文本字符串,而這些文本字符串本身包含在Text類型的子節點中。既然知道這些Text節點是唯一子元素,可以用getFirstChild方法而不用再遍歷一個NodeList,然後可以用getData方法檢索存儲在Text節點中的字符串。

			NodeList children = root.getChildNodes();
			for(int i = 0; i < children.getLength(); i++){
				Node child = children.item(i);
				if(child instanceof Element){
					Element childElement = (Element)child;
					Text textNode = (Text)childElement.getFirstChild();
					String text = textNode.getData().trim();
					...
				}
			}
注意:getData的返回值調用trim方法是個好主意,如下XML:

			<size>
				36
			</size>
那麼,解析器將會把所有的換行符和空格都包含到文本節點中去。調用trim方法可以把實際數據前後的空白字符刪掉。

如果要枚舉節點屬性,可調用getAttributes方法,返回一個NamedNodeMap對象,其中包含描述屬性的節點對象:

					NamedNodeMap attributes = element.getAttributes();
					for(int i = 0; i < attributes.getLength(); i++){
						Node attribute = attributes.item(i);
						String name = attribute.getNodeName();
						String value = attribute.getNodeValue();
						...
					}
或者,如果知道屬性名,則可以直接得到相應屬性值:

String unit = element.getAttribute("unit");


驗證XML文檔 

如果要規範文檔結構,可以提供一個文檔類型定義(DTD),DTD包含了用於解釋文檔是如何構成的規則 ,這些規則規範了每個元素的合法子元素和屬性。如:

<!ELEMENT font(name,size)>

這個規則表明,一個font元素總是有兩個子元素,分別是name和size。

具體DTD語法請參看Java核心技術或W3C。

對下一個XML文檔 :

<?xml version="1.0"?>
<!DOCTYPE staff[
	<!ELEMENT staff (employee)*>
	<!ELEMENT employee (name,salary)>
	<!ATTLIST employee nationality CDATA "china">
	<!ELEMENT name (#PCDATA)>
	<!ELEMENT salary (#PCDATA)>
]>
<staff>
	<employee nationality="china">
		<name>bin</name>
		<salary>500</salary>
	</employee>
	<manager nationality="china">
		<name>bin</name>
		<salary>500</salary>
	</manager>
</staff>
使用以下代碼進行解析:

import java.io.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

public class XMLDTDStudy {
	public static void main(String[] args) {
		try{
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			factory.setValidating(true);	//打開驗證特性
			factory.setIgnoringElementContentWhitespace(true);	//設置爲匆略文本節點的空白字符
			DocumentBuilder buider = factory.newDocumentBuilder();
			File f = new File("staffWithDTD.xml");
			Document doc = buider.parse(f);
			Element root = doc.getDocumentElement();
			NodeList children = root.getChildNodes();
			for(int i = 0; i < children.getLength(); i++){
				Node child = children.item(i);
				System.out.println(i + ":  " + child.getNodeName());
			}
		}catch(ParserConfigurationException e){
			e.printStackTrace();
		}catch(IOException e){
			e.printStackTrace();
		}catch(SAXException e){
			e.printStackTrace();
		}
	}
}
得到結果如下:

程序報告了manager元素並沒有在DTD中聲明並忽略空白字符。

使用XPath定位信息

<?xml version="1.0"?>
<staff>
    <employee nationality="china">
        <name>bin</name>
        <salary>500</salary>
    </employee>
    <employee>
        <name>zhou</name>
        <salary>800</salary>
    </employee>
</staff>

XPath可以描述XML文檔中的一組節點,如/staff/employee則描述了根元素staff的子元素中所有的employee元素。可以用[]操作符選擇特定元素:/staff/employee[1]表示選擇第一行(索引號從1開始)

使用@操作可以得到屬性值,如:"/staff/employee[1]/@nationality"得到第一個員工的國籍china

XPath有很多有用的函數,如:"count(/staff/employee)"返回根元素staff的子元素中employee元素的數量。

Java SE5.0增加了一個API計算XPath表達式,先從XPathFactory對象創建一個XPath對象。

         XPathFactory xpfactory = XPathFactory.newInstance();

            XPath path = xpfactory.newXPath();


然後,調用evaluate方法計算XPath對象表達 :

String name = path.evaluate("/staff/employee[1]/salary", doc);

如果XPath表達式產生一組節點,則如下調用:

NodeList nodes = (NodeList)path.evaluate("/staff/employee", doc,XPathConstants.NODESET);

如果結果只有一個節點,則如下調用:

Node node = (Node) path.evaluate("/staff/employee[1]", doc,XPathConstants.NODE);

如果結果是一個數字,則使用

int salary = ((Number) path.evaluate("/staff/employee[1]/salary", doc,XPathConstants.NUMBER)).intValue();

不必從文檔的根節點開始搜索,可以從任意一個節點或節點列表開始,如果你有前一個計算得到的一個節點node,就可以調用:

result = path.evaluate(expression,node);

               DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder buider = factory.newDocumentBuilder();
			File f = new File("staff.xml");
			Document doc = buider.parse(f);
			
			XPathFactory xpfactory = XPathFactory.newInstance();
			XPath path = xpfactory.newXPath();
			NodeList nodes = (NodeList)path.evaluate("/staff/employee", doc,XPathConstants.NODESET);
			for(int i = 0; i < nodes.getLength(); i++){
				System.out.println(path.evaluate("name", nodes.item(i)));	//輸出每個員工的名字
			}
			
			System.out.println(path.evaluate("/staff/employee[1]/salary", doc));	//輸出第一個員工國籍
			System.out.println(path.evaluate("count(/staff/employee)", doc));	//得到員工總數
			int salary = ((Number) path.evaluate("/staff/employee[1]/salary", doc,XPathConstants.NUMBER)).intValue();	//得到第一個員工的工資

		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		catch(XPathExpressionException e){
			e.printStackTrace();
		}


使用命名空間

Java語言使用包來避免名字衝突。XML也有類似的命名空間機制 ,用於元素名和屬性名。

名字空間是由統一資源標識符(URI)來標識,如:http://www.w3.org/2001/XMLSchema

下面是一個典型例子:

<xsd:shecma xmlns:xsd="http://www.w3.org/2001/XMLSchema">
	<xsd:element name="gridbag" type="GridBagType">
	...
</xsd>
下面的屬性:xmlns:alias="namespaceURI"用於定義命名空間和別名,上面例子中別名爲xsd。這樣,xsd:schema實際上指的是“命名空間http://www.w3.org/2001/XMLSchema中的schema”

注意:只有子元素繼承了它們父元素的命名空間,而不帶顯式別名前綴的屬性不是命名空間的一部分,如:

<configuration xmlns="http://www.horstmann.com/corejava"
	xmlns:si="http://www.bipm.fr/enus/3_SI/si.html">
	<size value="210" si:unit="mm"/>
	...
</configuration>
在這個示例中,元素configuration和size是URI http://www.horstmann.com/corejava的命名空間的一部分。屬性si:unit是URI http://www.bipm.fr/enus/3_SI/si.html命名空間的一部分,然而,屬性值不是任何命名空間的一部分。

默認地,Sun公司的DOM解析器是關閉了命名空間處理特性的。要打開命名空間處理特性,可以調用DocumentBuilderFactory類的setNamespaceAware方法:

factory.setNamespaceAware(true);

這樣工作生產的所有生成器都支持命名空間了。每個節點有三個屬性:

  • 帶有別名前綴的限定名,由getNodeName和getTagName等方法返回。
  • 命名空間URI,由getNamescapceURI方法返回
  • 不帶別名前綴和命名空間的本地名,由getLocalName方法返回。
如解析以下元素:

<xsd:shecma xmlns:xsd="http://www.w3.org/2001/XMLSchema">

會得到:

  • 限定名爲xsd:shecma
  • 命名空間URI爲http://www.w3.org/2001/XMLSchema
  • 本地名爲shecma
注意:如果命名空間特性被關閉,getLocalName和getNamespaceURI方法將返回null。

如果XPath要解析有命名空間的XML,還需要一些工作,

首先將XML內容修改爲:

<?xml version="1.0"?>
<xsd:staff xmlns:xsd="http://www.test">
	<xsd:employee nationality="china">
		<name>bin</name>
		<salary>500</salary>
	</xsd:employee>
</xsd:staff>

再實現一個NamespaceContext接口,它做的工作是將文檔中提取命名空間:

import java.util.Iterator;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import org.w3c.dom.Document;

public class UniversalNamespaceResolver implements NamespaceContext {
    private Document sourceDocument;

    public UniversalNamespaceResolver(Document document) {
        sourceDocument = document;
    }

    public String getNamespaceURI(String prefix) {
        if (prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
            return sourceDocument.lookupNamespaceURI(null);
        } else {
            return sourceDocument.lookupNamespaceURI(prefix);
        }
    }

    public String getPrefix(String namespaceURI) {
        return sourceDocument.lookupPrefix(namespaceURI);
    }

    public Iterator getPrefixes(String namespaceURI) {
        // not implemented yet
        return null;
    }
}

測試代碼如下:
import java.io.*;
import javax.xml.parsers.*;
import javax.xml.xpath.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

public class DomStudy {

	public static void main(String[] args) {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setNamespaceAware(true);
		try {
			DocumentBuilder buider = factory.newDocumentBuilder();
			File f = new File("staff.xml");
			Document doc = buider.parse(f);
			
			XPathFactory xpfactory = XPathFactory.newInstance();
			XPath path = xpfactory.newXPath();
			path.setNamespaceContext(new UniversalNamespaceResolver(doc));	//設置XPath的命名空間
			
			NodeList nodes = (NodeList)path.evaluate("/xsd:staff/xsd:employee", doc,XPathConstants.NODESET);
			for(int i = 0; i < nodes.getLength(); i++){
				Node node = nodes.item(i);
				System.out.println(node.getNamespaceURI());
				System.out.println(node.getLocalName());
				System.out.println(node.getNodeName());
			}
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		catch(XPathExpressionException e){
			e.printStackTrace();
		}
	}
}
得到結果:

http://www.test
employee
xsd:employee


流機制解析器

當XML文檔很大時,並且處理算法非常簡單,可能在運行時解析節點,而不必看到所有的樹形結構時,使用DOM可能顯得效率低下,這時,應使用流機制解析器。

SAX解析器

SAX解析器在解析XML輸入的構件時就報告事件,但不會以任何方式存儲文檔,而由事件處理器處理數據。實際上,DOM解析器是在SAX解析器的基礎上建立起來的,它在接收到解析器事件時建立DOM樹。

在使用SAX解析器,需要一個處理器來定義不同的解析器事件的事件動作,ContentHandler接口定義了若干個回調方法,下面是最重要的幾個:

  • startElement和endElement在每當遇到起始事終止標籤時調用
  • characters每當遇到字符數據時調用
  • startDocument和endDocument分別在文檔開始和結束各調用一次。
如解析如下片斷時:

<font>
	<size units="ps">36</size>
</font>
解析器確保產生以下調用:

1.startElement ,元素名:font

2.startElement, 元素名:size , 屬性:units="pt"

3.characters, 內容:36

4.endElement, 元素名:size

5.endDocument, 元素名:font

處理器必須覆蓋這些方法,讓它們執行在解析文件時想要執行的動作。

注意:與DOM解析器一樣,命名空間處理特性默認關閉。

如果使用下面代碼處理上面帶有命名空間的staff.xml

import java.io.*;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

public class SAXStudy {
	public static void main(String[] args) {
		DefaultHandler handler = new DefaultHandler(){	//定義一個DefaultHandler,並覆蓋startElement方法,輸出相關信息
			public void startElement(String uri,  String localName,
					String qName,Attributes attributes)throws SAXException{
				System.out.println("URI:" + uri + "  LocalName:" + localName + "   qName:" + qName );
			}
		};
		
		SAXParserFactory factory = SAXParserFactory.newInstance();
		factory.setNamespaceAware(true);
		try{
			SAXParser saxParser = factory.newSAXParser();
			InputStream in = new FileInputStream("staff.xml");
			saxParser.parse(in, handler);
			in.close();
		}
		catch(ParserConfigurationException e){
			e.printStackTrace();
		}
		catch(SAXException e){
			e.printStackTrace();
		}
		catch(FileNotFoundException e){
			e.printStackTrace();
		}
		catch(IOException e){
			e.printStackTrace();
		}
	}
}

使用StAX解析器

StAX解析器是一種“拉解析器(pull parser)”,與安裝事件處理器不同,只需要使用下面這樣的基本循環來迭代所有的事件:

			InputStream in = new FileInputStream("staff.xml");
			XMLInputFactory factory  = XMLInputFactory.newFactory();
			XMLStreamReader parser = factory.createXMLStreamReader(in);
			while(parser.hasNext()){
				int event = parser.next();
				call parser methods to obtain event details
			}
如解析以下片斷:

<font>
	<size units="ps">36</size>
</font>

解析器將產生下面的事件:

1.START_ELEMENT, 元素名:font

2.CHARACTERS, 內容:空白字符

3.START_ELEMENT, 元素名:size

4.CHARACTERS, 內容:36

5.END_ELEMENT, 元素名:size

6.CHARACTERS, 內容:空白字符

7.END_ELEMENT, 元素名:font

下面是一個實現:

import java.io.*;
import javax.xml.namespace.QName;
import javax.xml.stream.*;

public class StAXTest {

	public static void main(String[] args) {
		try{	
			InputStream in = new FileInputStream("staff.xml");
			XMLInputFactory factory  = XMLInputFactory.newFactory();
			XMLStreamReader parser = factory.createXMLStreamReader(in);
			while(parser.hasNext()){
				int event = parser.next();
				if(event == XMLStreamConstants.START_ELEMENT){
					QName qname = parser.getName();
					System.out.println(qname.toString());
				}
			}
		}
		catch(FileNotFoundException e){
			e.printStackTrace();
		}
		catch(XMLStreamException e){
			e.printStackTrace();
		}
	}
}


生成XML文檔

通過調用DocumentBuilder類的newDocument方法得到一個空文檔:

Document doc = builder.newDocument();

使用Document類的createElement方法可以構建文檔裏的元素:

Element rootElement = doc.createElement(rootName);

Element childElement = doc.createElement(childName);

使用createTextNode方法構建文本節點:

Text textNode = doc.createTextNode(textContents);

使用以下方法給文檔加上根元素,給父結節加上子節點:

doc.appendChild(rootElement);

rootElement.appendChild(childElement);

childElement.appendChild(textNode);

調用Element類的setAttribute方法設置元素屬性:

rootElement.setAttrbute(name,value);

將doc輸出到文件中,可以使用Transformer類,通過transform輸出doc樹

import java.io.File;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.*;

public class WriteXML {
	public static void main(String[] args) {
		try{
			DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			Document doc = builder.newDocument();
			
			Element staff = doc.createElementNS("http://www.test", "xsd:staff");	//創建根結點
			Element employee = doc.createElement("xsd:employee");	//創建employee結點
			
			Element nameElem = doc.createElement("name");	//創建name結點
			Text nameText = doc.createTextNode("bin");	//創建文本結點
			Element salaryElem = doc.createElement("salary");
			Text salaryText = doc.createTextNode("500");
			
			//將結果組織到doc樹中
			doc.appendChild(staff);
			staff.appendChild(employee);
			employee.appendChild(nameElem);
			nameElem.appendChild(nameText);
			employee.appendChild(salaryElem);
			salaryElem.appendChild(salaryText);
			
			//將doc樹輸出到文件中
			Transformer t = TransformerFactory.newInstance().newTransformer();
			//設置輸出格式
			t.setOutputProperty(OutputKeys.METHOD,"xml");
			t.setOutputProperty(OutputKeys.INDENT, "yes");

			File f = new File("writerStaff.xml");
			t.transform(new DOMSource(doc), new StreamResult(f));
		}
		catch(ParserConfigurationException e){
			e.printStackTrace();
		}
		catch(TransformerConfigurationException e){
			e.printStackTrace();
		}
		catch(TransformerException e){
			e.printStackTrace();
		}
	}
}

使用StAXXML文檔 

StAX API使我們可以直接將XML樹寫出,先構建一個XMLStreamWriter:

XMLOutputFactory factory = XMLOutputFactory.newInstance();

XMLStreamWriter writer = factory.createXMLStreamWriter(out);

要產生XML文件頭,調用:

writer.writeStartDocument();

然後要產生元素則調用 :

writer.writeStartElement(name); 

添加屬性需要調用:

writer.writeAttribute(name,value);

寫出字符則調用:

writer.writeCharacters(text);

要寫出沒有子節點的元素可調用:

writer.writeEmptyElement); 

在添加完所有子節點後,調用:

writer.writeEndElement(); 

這會導致當前元素被關閉

最後,在文檔的結尾,調用

writer.writeEndDocument();

調用將關閉所有的元素。

注意:與使用DOM/XSLT方式一樣,不必擔心屬性值和字符數據的轉義字符。並且,StAX當前的版本還沒有任何對產生縮進輸出的支持。

下面是一個實例:

import java.io.*;
import javax.xml.stream.*;

public class StAXWriterXML {
	public static void main(String[] args) {
		try{	
			FileOutputStream out = new FileOutputStream("StAXWriterStaff.xml");
			XMLOutputFactory factory = XMLOutputFactory.newInstance();
			XMLStreamWriter writer = factory.createXMLStreamWriter(out);
			
			writer.writeStartDocument();
			writer.writeStartElement("staff");
			writer.writeStartElement("employee");
			writer.writeStartElement("name");
			writer.writeCharacters("bin");
			writer.writeEndElement();
			writer.writeEndElement();
			writer.writeEndDocument();

			writer.close();
			out.close();	
		}
		catch (XMLStreamException e) {
			e.printStackTrace();
		}
		catch(FileNotFoundException e){
			e.printStackTrace();
		}
		catch(IOException e){
			e.printStackTrace();
		}
	}
}

XSL轉換

XSL轉換 機制可以指定將XML文檔轉換爲其他格式的規則,例如,純文本,XHTML或其他任何XML格式。XSLT通常用於將一個機器可讀的XML格式轉譯爲另一種機器可讀的XML格式,或者將XML轉譯爲適於人類閱讀的表示格式。

具體方法請參看Java核心技術。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章