2011年11月29日火曜日

Solution for gdata-java-client's SAXNotRecognizedException

You will see SAXNotRecognizedException if you try to retrieve data from YouTube using gdata-java-client.

gdata-java-client
http://code.google.com/p/gdata-java-client/

The root cause is reported in detail as Issue 9493.

http://code.google.com/p/android/issues/detail?id=9493

The first solution that came to my mind is to replace SAXParserFactory using the system property "javax.xml.parsers.SAXParserFactory". This mechanism is described also in Android's online document.

http://developer.android.com/reference/javax/xml/parsers/SAXParser.html

So, I tried the following code to replace the implementation of SAXParserFactory with xerces's one.

System.setProperty("javax.xml.parsers.SAXParserFactory", "org.apache.xerces.jaxp.SAXParserFactoryImpl");

However, this did not work, and I finally found that Android's SAXParserFactory does not support the mechanism to replace the implementation.

http://www.java2s.com/Open-Source/Android/android-core/platform-libcore/javax/xml/parsers/SAXParserFactory.java.htm

/**
 * Returns Android's implementation of {@code SAXParserFactory}. Unlike
 * other Java implementations, this method does not consult system
 * properties, property files, or the services API.
 *
 * @return a new SAXParserFactory.
 *
 * @exception FactoryConfigurationError never. Included for API
 *     compatibility with other Java implementations.
 */
public static SAXParserFactory newInstance() {
    // instantiate the class directly rather than using reflection
    return new SAXParserFactoryImpl();
}

So, I returned back to the report of Issue 9493. It says that gdata-java-client defines and uses SecureGenericXMLFactory and it intentionally disables some features that are needed to parse YouTube responses.

The actual source code lines to disable the necessary features are written in the constructor of SecureSAXParserFactory, which is an internal class of SecureGenericXMLFactory.

/* Setting the attribute
 * http://apache.org/xml/features/disallow-doctype-decl to true causes an
 * immediate exception when a DTD is encountered. Unfortunately, an XML
 * document will sometimes include a harmless DTD so we cannot ban DTDs
 * outright.
 */
try {
  factory.setFeature(
    "http://xml.org/sax/features/external-general-entities",
    false);
} catch (IllegalArgumentException e) {
  /* OK.  Not all parsers will support this attribute */
} catch (SAXNotRecognizedException e) {
  /* OK.  Not all parsers will support this attribute */
}

try {
  factory.setFeature(
    "http://xml.org/sax/features/external-parameter-entities",false);
} catch (IllegalArgumentException e) {
  /* OK.  Not all parsers will support this attribute */
} catch (SAXNotRecognizedException e) {
  /* OK.  Not all parsers will support this attribute */
}

After seeing this fact, the solution I adopted was to replace the implementation of SecureGenericXMLFactory with my own. The concrete steps were as follows.

(1) Remove the original SecureGenericXMLFactory from gdata-core-1.0.jar

cd gdata-src.java-1.46.0/gdata/java/lib
mkdir gdata-core-1.0-no-SecureGenericXMLFactory
cd gdata-core-1.0-no-SecureGenericXMLFactory
jar xf ../gdata-core-1.0.jar
rm -rf com/google/gdata/util/common/xml/parsing
jar cfm ../gdata-core-1.0-no-SecureGenericXMLFactory.jar META-INF/MANIFEST.MF com

(2) Replace the original JAR file with the new one created by the step (1) in Eclipse.

(3) Copy the original source code of SecureGenericXMLFactory to my Android project.

(4) Add an unconditional 'return' in the constructor of SecureSAXParserFactory before the code lines that disable the necessary features.

// "if (true)" was added to avoid a compilation error in Eclipse.
if (true)
{
    return;
}


The detailed report of Issue 9493 was really helpful. I want to thank the reporter.





gdata-java-client の SAXNotRecognizedException に対する解決法

gdata-java-client を用いて YouTube からデータを取得しようとすると、SAXNotRecognizedException が発生してしまう。

gdata-java-client
http://code.google.com/p/gdata-java-client/

根本原因は Issue 9493 に詳細に報告されている。

http://code.google.com/p/android/issues/detail?id=9493

最初に思いついた解決法は、システムプロパティ "javax.xml.parsers.SAXParserFactory" を用いて SAXParcerFactory を置き換えることだった。

http://developer.android.com/reference/javax/xml/parsers/SAXParser.html

そこで、SAXParserFactory の実装を xerces のもので置き換えるために次のコードを試してみた。

System.setProperty("javax.xml.parsers.SAXParserFactory", "org.apache.xerces.jaxp.SAXParserFactoryImpl");

しかし、これは機能せず、最終的に Anroid の SAXParserFactory が実装を置き換えるメカニズムをサポートしていないことが分かった。

http://www.java2s.com/Open-Source/Android/android-core/platform-libcore/javax/xml/parsers/SAXParserFactory.java.htm

/**
 * Returns Android's implementation of {@code SAXParserFactory}. Unlike
 * other Java implementations, this method does not consult system
 * properties, property files, or the services API.
 *
 * @return a new SAXParserFactory.
 *
 * @exception FactoryConfigurationError never. Included for API
 *     compatibility with other Java implementations.
 */
public static SAXParserFactory newInstance() {
    // instantiate the class directly rather than using reflection
    return new SAXParserFactoryImpl();
}

そこで、Issue 9493 のレポートに戻ることにした。レポートによると、gdata-java-client は SecureGenericXMLFactory を定義して使っており、それが、YouTube のレスポンスをパースするのに必要となるフィーチャーを意図的に無効にしている、とのこと。

必要なフィーチャーを無効にしている実際のソースコード行は、SecureGenericXMLFactory の内部クラスである SecureSAXParserFactory のコンストラクタに書かれている。

/* Setting the attribute
 * http://apache.org/xml/features/disallow-doctype-decl to true causes an
 * immediate exception when a DTD is encountered. Unfortunately, an XML
 * document will sometimes include a harmless DTD so we cannot ban DTDs
 * outright.
 */
try {
  factory.setFeature(
    "http://xml.org/sax/features/external-general-entities",
    false);
} catch (IllegalArgumentException e) {
  /* OK.  Not all parsers will support this attribute */
} catch (SAXNotRecognizedException e) {
  /* OK.  Not all parsers will support this attribute */
}

try {
  factory.setFeature(
    "http://xml.org/sax/features/external-parameter-entities",false);
} catch (IllegalArgumentException e) {
  /* OK.  Not all parsers will support this attribute */
} catch (SAXNotRecognizedException e) {
  /* OK.  Not all parsers will support this attribute */
}

この事実から、私が選んだ解決法は、SecureGenericXMLFactory の実装を自分のもので置き換えることだった。具体的な手順は次の通り。

(1) オリジナルの SecureGenericXMLFactory を gdata-core-1.0.jar から削除する。

cd gdata-src.java-1.46.0/gdata/java/lib
mkdir gdata-core-1.0-no-SecureGenericXMLFactory
cd gdata-core-1.0-no-SecureGenericXMLFactory
jar xf ../gdata-core-1.0.jar
rm -rf com/google/gdata/util/common/xml/parsing
jar cfm ../gdata-core-1.0-no-SecureGenericXMLFactory.jar META-INF/MANIFEST.MF com

(2) Eclipse で、オリジナルの JAR ファイルを手順 (1) で作成した新しいもので置き換える。

(3) オリジナルの SecureGenericXMLFactory のソースコードを自分の Android プロジェクトにコピーする。

(4) SecureSAXParserFactory のコンストラクタ内の、必要なフィーチャーを無効にしているコード行の前に、無条件の return を追加する。

// "if (true)" はEclipseでのコンパイルエラーを避けるために追加
if (true)
{
    return;
}


Issue 9493 の詳細なレポートはとても役に立った。報告者に感謝したいと思う。