android-ibc-forum

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 4a9e29af734c48da86403dabfecff3f8c31106aa
parent 262fa7d2360f634a78a6ae3b5e6ce63dd7657f18
Author: Jan Dankert <devnull@localhost>
Date:   Wed, 18 Jan 2012 23:35:30 +0100

Initiale Version incl. XML-RPC-Bibliothek, RSS-Reader-Bibliothek. Funktionsfähig: News, Fotos (rudimentär!).

Diffstat:
AndroidManifest.xml | 30++++++++++++++++++++++++++++++
res/drawable/ibc_icon.png | 0
res/drawable/ibc_small.png | 0
res/drawable/ibc_wallpaper.png | 0
res/layout/main.xml | 24++++++++++++++++++++++++
res/menu/main.xml | 6++++++
res/values/ibc.xml | 20++++++++++++++++++++
res/values/strings.xml | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
res/xml/preferences.xml | 18++++++++++++++++++
src/de/mtbnews/android/Configuration.java | 15+++++++++++++++
src/de/mtbnews/android/ForumActivity.java | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/de/mtbnews/android/IBCActivity.java | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/de/mtbnews/android/NewsActivity.java | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/de/mtbnews/android/PhotoActivity.java | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/de/mtbnews/android/adapter/RSSContentAdapter.java | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/de/mtbnews/android/service/UploadIntentService.java | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/de/mtbnews/android/util/ExceptionUtils.java | 27+++++++++++++++++++++++++++
src/de/mtbnews/android/util/IBC.java | 32++++++++++++++++++++++++++++++++
src/de/mtbnews/android/util/ServerAsyncTask.java | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/Dates.java | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/MediaAttributes.java | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/MediaThumbnail.java | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSBase.java | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSConfig.java | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSException.java | 47+++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSFault.java | 44++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSFeed.java | 45+++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSHandler.java | 311+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSItem.java | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSLoader.java | 455+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSParser.java | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSParserSPI.java | 37+++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSReader.java | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSReaderException.java | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/Resources.java | 50++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/xmlrpc/android/Base64Coder.java | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/xmlrpc/android/IXMLRPCSerializer.java | 32++++++++++++++++++++++++++++++++
src/org/xmlrpc/android/MethodCall.java | 20++++++++++++++++++++
src/org/xmlrpc/android/Tag.java | 14++++++++++++++
src/org/xmlrpc/android/XMLRPCClient.java | 559+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/xmlrpc/android/XMLRPCCommon.java | 43+++++++++++++++++++++++++++++++++++++++++++
src/org/xmlrpc/android/XMLRPCException.java | 30++++++++++++++++++++++++++++++
src/org/xmlrpc/android/XMLRPCFault.java | 24++++++++++++++++++++++++
src/org/xmlrpc/android/XMLRPCSerializable.java | 17+++++++++++++++++
src/org/xmlrpc/android/XMLRPCSerializer.java | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/xmlrpc/android/XMLRPCServer.java | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
46 files changed, 3998 insertions(+), 0 deletions(-)

diff --git a/AndroidManifest.xml b/AndroidManifest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + android:versionCode="1" + android:versionName="1.0" package="de.mtbnews.android"> + + <application android:icon="@drawable/ibc_icon" android:label="@string/app_name" android:theme="@style/IBC"> + <activity android:name=".IBCActivity" android:label="@string/app_name"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + <activity android:name="Configuration" android:label="@string/preferences"></activity> + + <activity android:name="ForumActivity" android:label="@string/forum"></activity> + <activity android:name="NewsActivity" android:label="@string/news"></activity> + <activity android:name="PhotoActivity" android:label="@string/photos"></activity> + + <service android:name=".service.UploadIntentService"></service> + +</application> + + <uses-permission android:name="android.permission.INTERNET" /> + + <!-- Android 1.6 --> + <uses-sdk android:minSdkVersion="4"></uses-sdk> + +</manifest> + \ No newline at end of file diff --git a/res/drawable/ibc_icon.png b/res/drawable/ibc_icon.png Binary files differ. diff --git a/res/drawable/ibc_small.png b/res/drawable/ibc_small.png Binary files differ. diff --git a/res/drawable/ibc_wallpaper.png b/res/drawable/ibc_wallpaper.png Binary files differ. diff --git a/res/layout/main.xml b/res/layout/main.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" android:layout_height="wrap_content"> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <TextView android:layout_width="fill_parent" android:id="@+id/hello" + android:layout_height="wrap_content" /> + + <Button android:id="@+id/forum" android:text="@string/forum" + android:layout_width="fill_parent" android:layout_height="wrap_content"></Button> + + <Button android:id="@+id/news" android:text="@string/news" + android:layout_width="fill_parent" android:layout_height="wrap_content"></Button> + + <Button android:id="@+id/photo" android:text="@string/photos" + android:layout_width="fill_parent" android:layout_height="wrap_content"></Button> + + </LinearLayout> + +</ScrollView>+ \ No newline at end of file diff --git a/res/menu/main.xml b/res/menu/main.xml @@ -0,0 +1,5 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/menu_preferences" android:title="@string/preferences" + android:icon="@android:drawable/ic_menu_preferences"></item> +</menu> + + \ No newline at end of file diff --git a/res/values/ibc.xml b/res/values/ibc.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="IBC" parent="android:Theme"> + + <item name="android:textColor">#003399</item> + <item name="android:colorBackground">#FFCC66</item> + + <item name="android:button">@style/IBCButton</item> + </style> + + <style name="IBCButton" parent="android:style/Widget.Button"> + <item name="android:textSize">19sp</item> + <!-- + <item name="android:layout_margin">0dip</item> + --> + <item name="android:background">#E3E3E3</item> + <item name="android:textColor">#003399</item> + </style> +</resources>+ \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">IBC</string> + <string name="preferences">Einstellungen</string> + <string name="username">Benutzername</string> + <string name="path">Pfad</string> + + <string name="hostname">Hostname</string> + <string name="password">Kennwort</string> + <string name="port">Port</string> + <string name="version">Version</string> + <string name="connect">Mit Server verbinden</string> + <string-array name="api_list"> + <item>1</item> + <item>2</item> + + </string-array> + <string-array name="version_list"> + <item>1.0</item> + <item>1.1</item> + </string-array> + <string name="writable">Schreibzugriff</string> + <string name="version_summary">Server-Version</string> + + <string name="publish">Veröffentlichen</string> + <string name="properties">Eigenschaften</string> + <string name="loading">Warten auf Server</string> + <string name="waitingforcontent">Inhalt wird geladen</string> + <string name="waitingforprojects">Projekte werden geladen</string> + <string name="waitingforlogin">Anmeldung am Server</string> + <string name="waitingfordelete">Löschen</string> + <string name="waitingfornew">Erzeuge</string> + <string name="waitingforsave">Speichern</string> + <string name="waitingforlanguageload">Verfügbare Sprachen werden geladen</string> + <string name="waitingforlanguagesave">Ausgewählte Sprache wird aktiviert</string> + <string name="language">Sprache</string> + <string name="model">Variante</string> + <string name="new1">Neu</string> + <string name="new_folder">Neuer Unterordner</string> + <string name="new_page">Neue Seite</string> + <string name="upload">Hochladen</string> + <string name="upload_long">Die Datei wird jetzt hochladen</string> + <string name="upload_file">Datei hochladen</string> + <string name="upload_image">Bild hochladen</string> + <string name="upload_ok">Hochladevorgang abgeschlossen</string> + <string name="upload_ok_long">Die Datei wurde erfolgreich auf den Server geladen</string> + <string name="upload_fail">Hochladevorgang fehlerhaft</string> + <string name="upload_fail_long">Die Datei konnte nicht auf den Server geladen werden</string> + <string name="publish_long">Die Veröffentlichung wurde gestartet</string> + <string name="publish_ok">Wurde veröffentlicht</string> + <string name="publish_ok_long">Erfolgreich veröffentlicht</string> + <string name="publish_fail">Veröffentlichung nicht erfolgreich</string> + <string name="publish_fail_long">Leider konnte die Datei nicht veröffentlicht werden</string> + <string name="delete">Löschen</string> + <string name="name">Name</string> + <string name="filename">Dateiname</string> + <string name="template">Vorlage</string> + <string name="save">Speichern</string> + + + + + <string name="description">Inhalt</string> + <string name="editor">Inhalt</string> + <string name="error_timeout">Zeitablauf</string> + <string name="error_io">Verbindungsabbruch</string> + <string name="error">Fehler</string> + <string name="reason">Grund</string> + <string name="areyousure">Sicher?</string> + <string name="choosefile">Datei auswählen</string> + <string name="chooseimage">Bild auswählen</string> + <string name="newserver">Neuer Server</string> + <string name="server">Server</string> + <string name="timeout">Zeitablauf</string> + <string name="timeout_desc">Timeout der TCP/IP-Verbindung in Sekunden</string> + <string name="databaseid">Datenbank-Id</string> + <string name="databaseid_desc">Datenbank-Id aus der Server-Konfiguration</string> + <string name="saved">Gespeichert</string> + <string name="release">Freigeben</string> + <string name="noserver">Es ist noch kein Server konfiguriert. Über die Menütaste können Sie einen neuen Server anlegen.</string> + +<string name="forum">Forum</string> +<string name="news">News</string> +<string name="photos">Fotos</string> +</resources> diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" + android:title="@string/preferences"> + + <!-- + <EditTextPreference android:key="timeout" + android:title="@string/timeout" android:summary="@string/timeout_desc" + android:defaultValue="30"></EditTextPreference> + --> + <!-- + <EditTextPreference android:key="server" + android:title="@string/server"></EditTextPreference> + --> + <EditTextPreference android:key="username" + android:title="@string/username"></EditTextPreference> + <EditTextPreference android:key="password" + android:password="true" android:title="@string/password"></EditTextPreference> +</PreferenceScreen> diff --git a/src/de/mtbnews/android/Configuration.java b/src/de/mtbnews/android/Configuration.java @@ -0,0 +1,15 @@ +package de.mtbnews.android; + +import android.os.Bundle; +import android.preference.PreferenceActivity; + +public class Configuration extends PreferenceActivity +{ + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.preferences); + } +} diff --git a/src/de/mtbnews/android/ForumActivity.java b/src/de/mtbnews/android/ForumActivity.java @@ -0,0 +1,84 @@ +/** + * + */ +package de.mtbnews.android; + +import java.io.IOException; +import java.lang.reflect.ReflectPermission; +import java.util.Map; + +import org.xmlrpc.android.XMLRPCClient; +import org.xmlrpc.android.XMLRPCException; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import de.mtbnews.android.util.IBC; +import de.mtbnews.android.util.ServerAsyncTask; + +/** + * @author dankert + * + */ +public class ForumActivity extends Activity +{ + public static final String ID = "id"; + public static final String CLIENT = "client"; + private String objectid; + + Map<String, String> data; + + @Override + protected void onCreate(Bundle savedInstanceState) + { + final SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(this); + + super.onCreate(savedInstanceState); + + setContentView(R.layout.listing); + + new ServerAsyncTask(this, R.string.waitingforcontent) + { + + @Override + protected void callServer() throws IOException + { + + XMLRPCClient client = new XMLRPCClient(IBC.IBC_FORUM_CONNECTOR_URL); + // add 2 to 4 + Object[] params = new Object[] { + prefs.getString("username", "").getBytes(), + prefs.getString("password", "").getBytes() }; + + try + { + Object sum = client.callEx("login", params); + System.out.println(sum.getClass()); + System.out.println(sum); + + Object l = client.call("get_inbox_stat"); + System.out.println(l.toString() ); + + Object i = client.call("get_box_info"); + System.out.println(i.toString() ); + + } + catch (XMLRPCException e) + { + e.printStackTrace(); + throw new RuntimeException(e); + } + + } + + protected void doOnSuccess() + { + } + + }.execute(); + + } + +} diff --git a/src/de/mtbnews/android/IBCActivity.java b/src/de/mtbnews/android/IBCActivity.java @@ -0,0 +1,135 @@ +/* + * IBC-App for Android + * + * Copyright (C) 2011 Jan Dankert + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.mtbnews.android; + +import java.util.ArrayList; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; + +/** + * @author Jan Dankert + */ +public class IBCActivity extends Activity +{ + private static final String PREFS_NAME = "OR_BLOG_PREFS"; + private List<String> serverList; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + SharedPreferences globalPrefs = PreferenceManager + .getDefaultSharedPreferences(this); + + ArrayList<String> list = new ArrayList<String>(); + if (globalPrefs.getString("username", "").equals("") ) + { + // Noch kein Benutzer konfiguriert. Hinweis anzeigen! + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(getResources().getString(R.string.noserver)); + AlertDialog alert = builder.create(); + alert.show(); + } + + Button forumButton = (Button) findViewById(R.id.forum); + forumButton.setOnClickListener( new OnClickListener() + { + + @Override + public void onClick(View v) + { + startActivity(new Intent(IBCActivity.this, ForumActivity.class)); + } + }); + + Button newsButton = (Button) findViewById(R.id.news); + newsButton.setOnClickListener( new OnClickListener() + { + + @Override + public void onClick(View v) + { + startActivity(new Intent(IBCActivity.this, NewsActivity.class)); + } + }); + + Button photoButton = (Button) findViewById(R.id.photo); + photoButton.setOnClickListener( new OnClickListener() + { + + @Override + public void onClick(View v) + { + startActivity(new Intent(IBCActivity.this, PhotoActivity.class)); + } + }); + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + super.onCreateOptionsMenu(menu); + MenuInflater mi = new MenuInflater(getApplication()); + mi.inflate(R.menu.main, menu); + + return true; + } + + public boolean onOptionsItemSelected(MenuItem item) + { + switch (item.getItemId()) + { + case R.id.menu_preferences: + startActivity(new Intent(this, Configuration.class)); + return true; + } + return false; + } + + @Override + protected void onStop() + { + super.onStop(); + + // Save user preferences. We need an Editor object to + // make changes. All objects are from android.context.Context + SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); + SharedPreferences.Editor editor = settings.edit(); + // editor.putBoolean("silentMode", mSilentMode); + + // Don't forget to commit your edits!!! + editor.commit(); + } +}+ \ No newline at end of file diff --git a/src/de/mtbnews/android/NewsActivity.java b/src/de/mtbnews/android/NewsActivity.java @@ -0,0 +1,86 @@ +package de.mtbnews.android; + +import java.io.IOException; +import java.util.Map; + +import org.apache.http.client.ClientProtocolException; +import org.mcsoxford.rss.RSSFeed; +import org.mcsoxford.rss.RSSItem; +import org.mcsoxford.rss.RSSReader; +import org.mcsoxford.rss.RSSReaderException; + +import android.app.ListActivity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.AdapterView.OnItemClickListener; +import de.mtbnews.android.adapter.RSSContentAdapter; +import de.mtbnews.android.util.IBC; +import de.mtbnews.android.util.ServerAsyncTask; + +public class NewsActivity extends ListActivity +{ + public final static String ELEMENTID = "elementid"; + public final static String OBJECTID = "objectid"; + // public final static String TYPE = "type"; + public static final String CLIENT = "client"; + private Map<String, String> properties; + + @Override + protected void onCreate(Bundle savedInstanceState) + { + setContentView(R.layout.listing); + + super.onCreate(savedInstanceState); + + new ServerAsyncTask(this, R.string.waitingforcontent) + { + + private RSSFeed feed; + + @Override + protected void callServer() throws IOException + { + RSSReader reader = new RSSReader(); + try + { + feed = reader.load(IBC.IBC_NEWS_RSS_URL); + } + catch (RSSReaderException e) + { + throw new ClientProtocolException(e); + } + } + + protected void doOnSuccess() + { + ListAdapter adapter = new RSSContentAdapter(NewsActivity.this, + feed); + NewsActivity.this.setTitle( feed.getTitle() ); + setListAdapter(adapter); + } + }.execute(); + + final ListView list = getListView(); + + list.setOnItemClickListener(new OnItemClickListener() + { + + @Override + public void onItemClick(AdapterView<?> parent, View view, + int position, long id) + { + RSSItem item = (RSSItem) getListAdapter().getItem(position); + + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(item.getLink()); + startActivity(i); + } + }); + + } +} diff --git a/src/de/mtbnews/android/PhotoActivity.java b/src/de/mtbnews/android/PhotoActivity.java @@ -0,0 +1,87 @@ +package de.mtbnews.android; + +import java.io.IOException; +import java.util.Map; + +import org.apache.http.client.ClientProtocolException; +import org.mcsoxford.rss.RSSFeed; +import org.mcsoxford.rss.RSSItem; +import org.mcsoxford.rss.RSSReader; +import org.mcsoxford.rss.RSSReaderException; + +import android.app.ListActivity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.AdapterView.OnItemClickListener; +import de.mtbnews.android.adapter.RSSContentAdapter; +import de.mtbnews.android.util.IBC; +import de.mtbnews.android.util.ServerAsyncTask; + +public class PhotoActivity extends ListActivity +{ + public final static String ELEMENTID = "elementid"; + public final static String OBJECTID = "objectid"; + // public final static String TYPE = "type"; + public static final String CLIENT = "client"; + private Map<String, String> properties; + + @Override + protected void onCreate(Bundle savedInstanceState) + { + setContentView(R.layout.listing); + + super.onCreate(savedInstanceState); + + new ServerAsyncTask(this, R.string.waitingforcontent) + { + + private RSSFeed feed; + + @Override + protected void callServer() throws IOException + { + RSSReader reader = new RSSReader(); + try + { + feed = reader.load(IBC.IBC_FOTOS_RSS_URL); + } + catch (RSSReaderException e) + { + throw new ClientProtocolException("Feed not available", e); + } + } + + protected void doOnSuccess() + { + ListAdapter adapter = new RSSContentAdapter(PhotoActivity.this, + feed); + PhotoActivity.this.setTitle( feed.getTitle() ); + + setListAdapter(adapter); + } + }.execute(); + + final ListView list = getListView(); + + list.setOnItemClickListener(new OnItemClickListener() + { + + @Override + public void onItemClick(AdapterView<?> parent, View view, + int position, long id) + { + RSSItem item = (RSSItem) getListAdapter().getItem(position); + + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(item.getLink()); + startActivity(i); + + } + }); + + } +} diff --git a/src/de/mtbnews/android/adapter/RSSContentAdapter.java b/src/de/mtbnews/android/adapter/RSSContentAdapter.java @@ -0,0 +1,91 @@ +/** + * + */ +package de.mtbnews.android.adapter; + +import org.mcsoxford.rss.RSSFeed; +import org.mcsoxford.rss.RSSItem; + +import android.content.Context; +import android.text.Html; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; +import de.mtbnews.android.R; + +/** + * @author dankert + * + */ +public class RSSContentAdapter extends BaseAdapter +{ + + /** Remember our context so we can use it when constructing views. */ + private Context mContext; + + /** + * Hold onto a copy of the entire Contact List. + */ + + private LayoutInflater inflator; + + private RSSFeed feed; + + public RSSContentAdapter(Context context, RSSFeed feed) + { + mContext = context; + inflator = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + this.feed = feed; + } + + public int getCount() + { + return feed.getItems().size(); + } + + public Object getItem(int position) + { + return feed.getItems().get(position); + } + + /** Use the array index as a unique id. */ + public long getItemId(int position) + { + return position; + + } + + /** + * @param convertView + * The old view to overwrite, if one is passed + * @returns a ContactEntryView that holds wraps around an ContactEntry + */ + public View getView(int position, View convertView, ViewGroup parent) + { + + RSSItem e = feed.getItems().get(position); + + final View view = inflator.inflate(R.layout.rss_item, null); + + TextView datum = (TextView) view.findViewById(R.id.item_date); + datum.setText(DateFormat.getTimeFormat(parent.getContext()).format( + e.getPubDate())); + + TextView name = (TextView) view.findViewById(R.id.item_title); + name.setText(e.getTitle()); + + TextView desc = (TextView) view.findViewById(R.id.item_description); + + if (e.getContent() != null) + desc.setText(Html.fromHtml(e.getContent())); + else if (e.getDescription() != null) + desc.setText(e.getDescription() + " ..."); + + return view; + } + +} diff --git a/src/de/mtbnews/android/service/UploadIntentService.java b/src/de/mtbnews/android/service/UploadIntentService.java @@ -0,0 +1,97 @@ +/** + * + */ +package de.mtbnews.android.service; + +import java.io.File; +import java.io.IOException; + +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import de.mtbnews.android.NewsActivity; +import de.mtbnews.android.R; + +/** + * @author dankert + * + */ +public class UploadIntentService extends IntentService +{ + + public static final String EXTRA_REQUEST = "request"; + public static final String EXTRA_FILENAME = "file"; + private static final int NOTIFICATION_UPLOAD = 1; + + public UploadIntentService() + { + super("UploadIntentService"); + } + + /** + * + * {@inheritDoc} + * + * @see android.app.IntentService#onHandleIntent(android.content.Intent) + */ + @Override + protected void onHandleIntent(Intent intent) + { + final String filePath = intent.getStringExtra(EXTRA_FILENAME); + + final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + final Intent notificationIntent = new Intent(this, NewsActivity.class); + final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, + notificationIntent, 0); + + final File file = new File(filePath); + final String tickerText = getResources() + .getString(R.string.upload_long); + final Notification notification = new Notification( + R.drawable.ibc_small, tickerText, System.currentTimeMillis()); + notification.setLatestEventInfo(getApplicationContext(), getResources() + .getString(R.string.upload), file.getName(), contentIntent); + notification.flags = Notification.FLAG_ONGOING_EVENT + | Notification.FLAG_NO_CLEAR; + nm.notify(NOTIFICATION_UPLOAD, notification); + + try + { + + // Alles OK. + final String msgText = getResources().getString(R.string.upload_ok); + notification.tickerText = getResources().getString( + R.string.upload_ok_long); + notification.setLatestEventInfo(getApplicationContext(), msgText, + file.getName(), contentIntent); + notification.flags = Notification.FLAG_AUTO_CANCEL; + nm.notify(NOTIFICATION_UPLOAD, notification); + Log.d(this.getClass().getName(), msgText); + throw new IOException(); + } + catch (IOException e) + { + // Fehler ist aufgetreten. + final String msgText = getResources().getString( + R.string.upload_fail); + notification.tickerText = getResources().getString( + R.string.upload_fail_long); + notification.setLatestEventInfo(getApplicationContext(), msgText, e + .getMessage() + + ": " + file.getName(), contentIntent); + notification.flags = Notification.FLAG_AUTO_CANCEL; + nm.notify(NOTIFICATION_UPLOAD, notification); + + Log.e(this.getClass().getName(), msgText, e); + } + finally + { + } + } + +} diff --git a/src/de/mtbnews/android/util/ExceptionUtils.java b/src/de/mtbnews/android/util/ExceptionUtils.java @@ -0,0 +1,27 @@ +/** + * + */ +package de.mtbnews.android.util; + +import java.io.IOException; +import java.net.SocketTimeoutException; + +import de.mtbnews.android.R; + +/** + * @author dankert + * + */ +public class ExceptionUtils +{ + + public static int getResourceStringId( Throwable throwable) { + + if ( throwable instanceof SocketTimeoutException ) + return R.string.error_timeout; + else if ( throwable instanceof IOException ) + return R.string.error_io; + else + return R.string.error_io; + } +} diff --git a/src/de/mtbnews/android/util/IBC.java b/src/de/mtbnews/android/util/IBC.java @@ -0,0 +1,32 @@ +package de.mtbnews.android.util; + +/** + * Statische Resourcen für IBC-Forum. + * + * @author dankert + * + */ +public interface IBC +{ + + /** + * Tapatalk-API für IBC-Forum. + */ + static final String IBC_FORUM_CONNECTOR_URL = "http://www.mtb-news.de/forum/mobiquo/mobiquo.php"; + + /** + * Feed-URL für Forum. + */ + static final String IBC_FORUM_RSS_URL = "http://www.mtb-news.de/forum/external.php?type=RSS2"; + + /** + * Fee-URL für Nachrichten. + */ + static final String IBC_NEWS_RSS_URL = "http://www.mtb-news.de/news/feed/"; + + /** + * Feed-URL für Fotos. + */ + static final String IBC_FOTOS_RSS_URL = "http://fotos.mtb-news.de/photos/recent.rss"; + +} diff --git a/src/de/mtbnews/android/util/ServerAsyncTask.java b/src/de/mtbnews/android/util/ServerAsyncTask.java @@ -0,0 +1,150 @@ +/** + * + */ +package de.mtbnews.android.util; + +import java.io.IOException; + + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.os.AsyncTask; +import android.util.Log; + +/** + * Ein asynchroner Task für den Zugriff auf einen Server. Der Aufruf + * des Servers muss in der zu überschreibenden Methode {@link #callServer()} + * durchgeführt werden.<br> + * <br> + * <br> + * Während der Serverabfrage wird ein {@link ProgressDialog} angezeigt. Falls + * die Abfrage nicht erfolgreich ist, wird automatisch ein {@link AlertDialog} + * mit einer Fehlermeldung angezeigt.<br> + * <br> + * <br> + * Durch überschreiben von {@link #doOnError(IOException)} kann selber auf einen + * Fehler reagiert werden. Durch Überschreiben von {@link #doOnSuccess()} kann + * eine Aktion nach erfolgreicher Serveranfrage ausgeführt werden. <br> + * + * @author dankert + * + */ +public abstract class ServerAsyncTask extends + AsyncTask<Void, Void, Void> +{ + private ProgressDialog progressDialog; + private Context context; + private AlertDialog alertDialog; + private IOException error; + + /** + * @param context + * Context des Aufrufers + * @param message + * Resource-Id für den Text im {@link ProgressDialog}. + */ + public ServerAsyncTask(Context context, int message) + { + this.context = context; + + this.progressDialog = new ProgressDialog(context); + // progressDialog.setTitle(R.string.loading); + progressDialog.setMessage(context.getResources().getString(message)); + } + + @Override + final protected void onPreExecute() + { + progressDialog.show(); + } + + /** + * {@inheritDoc} + * + * @see android.os.AsyncTask#onPostExecute(java.lang.Object) + */ + @Override + final protected void onPostExecute(Void result) + { + progressDialog.dismiss(); + + if (error != null) + { + doOnError(error); + } + else + { + doOnSuccess(); + } + } + + /** + * Wird aufgerufen, falls die Serveranfrage nicht durchgeführt werden + * konnte. Läuft im UI-Thread. + * + * @param error + * Exception, die aufgetreten ist. + */ + protected void doOnError(IOException error) + { + final Builder builder = new AlertDialog.Builder(this.context); + alertDialog = builder.setCancelable(true).create(); + final int causeRId = ExceptionUtils.getResourceStringId(error); + String msg = // this.context.getResources().getString(R.string.reason) + // + ":\n\n" + + error.getMessage(); + + Throwable t = error; + while (t.getCause() != null) + { + t = t.getCause(); + msg += ": " + t.getMessage(); + } + + alertDialog.setTitle(causeRId); + alertDialog.setIcon(android.R.drawable.ic_menu_close_clear_cancel); + alertDialog.setMessage(msg); + alertDialog.show(); + + } + + /** + * Wird aufgerufen, falls die Serveranfrage erfolgreich durchgeführt werden + * konnte. Läuft im UI-Thread. + */ + protected void doOnSuccess() + { + } + + /** + * Startet die Serveranfrage und fängt auftretene Fehler. + * + * @see android.os.AsyncTask#doInBackground(Params[]) + */ + @Override + final protected Void doInBackground(Void... params) + { + try + { + callServer(); + } + catch (IOException e) + { + Log.e(this.getClass().getName(), e.getMessage(), e); + error = e; + } + + return null; + } + + /** + * Ausführen der Serveranfrage. Auftretene {@link IOException} sollte + * weitergeworfen werden, da daraus ein {@link AlertDialog} erzeugt wird. + * + * @throws IOException + * Vom Server erzeugte Fehler + */ + protected abstract void callServer() throws IOException; +} diff --git a/src/org/mcsoxford/rss/Dates.java b/src/org/mcsoxford/rss/Dates.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +import java.text.ParseException; +import java.text.SimpleDateFormat; + +/** + * Internal helper class for date conversions. + * + * @author Mr Horn + */ +final class Dates { + + /** + * @see <a href="http://www.ietf.org/rfc/rfc0822.txt">RFC 822</a> + */ + private static final SimpleDateFormat RFC822 = new SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss Z", java.util.Locale.ENGLISH); + + /* Hide constructor */ + private Dates() {} + + /** + * Parses string as an RFC 822 date/time. + * + * @throws RSSFault if the string is not a valid RFC 822 date/time + */ + static java.util.Date parseRfc822(String date) { + try { + return RFC822.parse(date); + } catch (ParseException e) { + throw new RSSFault(e); + } + } + +} + diff --git a/src/org/mcsoxford/rss/MediaAttributes.java b/src/org/mcsoxford/rss/MediaAttributes.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +/** + * Internal helper class for RSS 2.0 media attributes. + * + * @author Mr Horn + */ +final class MediaAttributes { + + /* Hide constructor */ + private MediaAttributes() {} + + /** + * Returns the RSS 2.0 attribute with the specified local name as a string. + * The return value is {@code null} if no attribute with such name exists. + */ + static String stringValue(org.xml.sax.Attributes attributes, String name) { + return attributes.getValue(name); + } + + /** + * Returns the RSS 2.0 attribute with the specified local name as an integer. + * The {@code defaultValue} is returned if no attribute with such name exists. + */ + static int intValue(org.xml.sax.Attributes attributes, String name, int defaultValue) { + final String value = stringValue(attributes, name); + if(value == null) { + return defaultValue; + } + + return Integer.parseInt(value); + } + +} + diff --git a/src/org/mcsoxford/rss/MediaThumbnail.java b/src/org/mcsoxford/rss/MediaThumbnail.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +/** + * Immutable class for media thumbnail RSS 2.0 data. + * + * @author Mr Horn + * @see http://search.yahoo.com/mrss/ + */ +public final class MediaThumbnail { + + private final android.net.Uri url; + private final int height; + private final int width; + + /** + * Returns the URL of the thumbnail. + * The return value is never {@code null}. + */ + public android.net.Uri getUrl() { + return url; + } + + /** + * Returns the thumbnail's height or {@code -1} if unspecified. + */ + public int getHeight() { + return height; + } + + /** + * Returns the thumbnail's width or {@code -1} if unspecified. + */ + public int getWidth() { + return width; + } + + /* Internal constructor for RSSHandler */ + MediaThumbnail(android.net.Uri url, int height, int width) { + this.url = url; + this.height = height; + this.width = width; + } + + /** + * Returns the thumbnail's URL as a string. + */ + public String toString() { + return url.toString(); + } + + /** + * Returns the hash code of the thumbnail's URL. + */ + @Override + public int hashCode() { + return url.hashCode(); + } + + /** + * Compares the URLs of two thumbnails for equality. + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object instanceof MediaThumbnail) { + final MediaThumbnail other = (MediaThumbnail) (object); + + /* other is not null */ + return url.equals(other.url); + } else { + return false; + } + } + +} + diff --git a/src/org/mcsoxford/rss/RSSBase.java b/src/org/mcsoxford/rss/RSSBase.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +import java.util.ArrayList; + +/** + * Common data about RSS feeds and items. + * + * @author Mr Horn + */ +abstract class RSSBase { + + private String title; + private android.net.Uri link; + private String description; + private java.util.List<String> categories; + private java.util.Date pubdate; + + /** + * Specify initial capacity for the List which contains the category names. + */ + RSSBase(byte categoryCapacity) { + categories = categoryCapacity == 0 ? null : new ArrayList<String>( + categoryCapacity); + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public android.net.Uri getLink() { + return link; + } + + public java.util.List<String> getCategories() { + if (categories == null) { + return java.util.Collections.emptyList(); + } + + return java.util.Collections.unmodifiableList(categories); + } + + public java.util.Date getPubDate() { + return pubdate; + } + + void setTitle(String title) { + this.title = title; + } + + void setLink(android.net.Uri link) { + this.link = link; + } + + void setDescription(String description) { + this.description = description; + } + + void addCategory(String category) { + if (categories == null) { + categories = new ArrayList<String>(3); + } + + this.categories.add(category); + } + + void setPubDate(java.util.Date pubdate) { + this.pubdate = pubdate; + } + + /** + * Returns the title. + */ + public String toString() { + return title; + } + + /** + * Returns the hash code of the link. + */ + @Override + public int hashCode() { + if (link == null) { + return 0; + } + + return link.hashCode(); + } + + /** + * Compares the links for equality. + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object instanceof RSSBase) { + /* other is never null */ + final RSSBase other = (RSSBase) (object); + + if (link == null) { + return other.link == null; + } + + return link.equals(other.link); + } else { + return false; + } + } + +} + diff --git a/src/org/mcsoxford/rss/RSSConfig.java b/src/org/mcsoxford/rss/RSSConfig.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +/** + * Immutable data structure to configure the RSS parser and loader modules. On + * large data sets, well-chosen configuration values can reduce memory + * consumption and increase performance. + * + * @author Mr Horn + */ +public final class RSSConfig { + + /** + * Average number of RSS item &lt;category&gt; elements which serves as the + * initial capacity of the List implementation. + */ + final byte categoryAvg; + + /** + * Average number of RSS item &lt;media:thumbnail&gt; elements which serves as + * the initial capacity of the List implementation. + */ + final byte thumbnailAvg; + + /** + * Instantiate an RSS configuration with the specified parameters. + * + * @param categoryAvg average number of RSS item &lt;category&gt; elements in + * a typical RSS feed + * @param thumbnailAvg average number of RSS item &lt;metia:thumbnail&gt; + * elements in a typical RSS feed + */ + public RSSConfig(byte categoryAvg, byte thumbnailAvg) { + this.categoryAvg = categoryAvg; + this.thumbnailAvg = thumbnailAvg; + } + + /** + * Instantiate an RSS configuration with default values. + */ + public RSSConfig() { + this.categoryAvg = 3; + this.thumbnailAvg = 2; + } + +} + diff --git a/src/org/mcsoxford/rss/RSSException.java b/src/org/mcsoxford/rss/RSSException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +/** + * Contingency to which an RSS client should react. Unlike {@link RSSFault} + * runtime exceptions, the occurrence of an {@link RSSException} denotes an + * alternative execution flow for which RSS clients should account. + * + * @author Mr Horn + * @see RSSFault + */ +public class RSSException extends Exception { + + /** + * Unsupported serialization + */ + private static final long serialVersionUID = 1L; + + public RSSException(String message) { + super(message); + } + + public RSSException(Throwable cause) { + super(cause); + } + + public RSSException(String message, Throwable cause) { + super(message, cause); + } + +} + diff --git a/src/org/mcsoxford/rss/RSSFault.java b/src/org/mcsoxford/rss/RSSFault.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +/** + * Non-recoverable runtime exception. + * + * @author Mr Horn + */ +public class RSSFault extends RuntimeException { + + /** + * Unsupported serialization + */ + private static final long serialVersionUID = 1L; + + public RSSFault(String message) { + super(message); + } + + public RSSFault(Throwable cause) { + super(cause); + } + + public RSSFault(String message, Throwable cause) { + super(message, cause); + } + +} + diff --git a/src/org/mcsoxford/rss/RSSFeed.java b/src/org/mcsoxford/rss/RSSFeed.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +/** + * Data about an RSS feed and its RSS items. + * + * @author Mr Horn + */ +public class RSSFeed extends RSSBase { + + private final java.util.List<RSSItem> items; + + RSSFeed() { + super(/* initial capacity for category names */ (byte) 3); + items = new java.util.LinkedList<RSSItem>(); + } + + /** + * Returns an unmodifiable list of RSS items. + */ + public java.util.List<RSSItem> getItems() { + return java.util.Collections.unmodifiableList(items); + } + + void addItem(RSSItem item) { + items.add(item); + } + +} + diff --git a/src/org/mcsoxford/rss/RSSHandler.java b/src/org/mcsoxford/rss/RSSHandler.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +/** + * Internal SAX handler to efficiently parse RSS feeds. Only a single thread + * must use this SAX handler. + * + * @author Mr Horn + */ +class RSSHandler extends org.xml.sax.helpers.DefaultHandler { + + /** + * Constant for XML element name which identifies RSS items. + */ + private static final String RSS_ITEM = "item"; + + /** + * Constant symbol table to ensure efficient treatment of handler states. + */ + private final java.util.Map<String, Setter> setters; + + /** + * Reference is never {@code null}. Visibility must be package-private to + * ensure efficiency of inner classes. + */ + final RSSFeed feed = new RSSFeed(); + + /** + * Reference is {@code null} unless started to parse &lt;item&gt; element. + * Visibility must be package-private to ensure efficiency of inner classes. + */ + RSSItem item; + + /** + * If not {@code null}, then buffer the characters inside an XML text element. + */ + private StringBuilder buffer; + + /** + * Dispatcher to set either {@link #feed} or {@link #item} fields. + */ + private Setter setter; + + /** + * Interface to store information about RSS elements. + */ + private static interface Setter {} + + /** + * Closure to change fields in POJOs which store RSS content. + */ + private static interface ContentSetter extends Setter { + + /** + * Set the field of an object which represents an RSS element. + */ + void set(String value); + + } + + /** + * Closure to change fields in POJOs which store information + * about RSS elements which have only attributes. + */ + private static interface AttributeSetter extends Setter { + + /** + * Set the XML attributes. + */ + void set(org.xml.sax.Attributes attributes); + + } + + /** + * Setter for RSS &lt;title&gt; elements inside a &lt;channel&gt; or an + * &lt;item&gt; element. The title of the RSS feed is set only if + * {@link #item} is {@code null}. Otherwise, the title of the RSS + * {@link #item} is set. + */ + private final Setter SET_TITLE = new ContentSetter() { + @Override + public void set(String title) { + if (item == null) { + feed.setTitle(title); + } else { + item.setTitle(title); + } + } + }; + + /** + * Setter for RSS &lt;description&gt; elements inside a &lt;channel&gt; or an + * &lt;item&gt; element. The title of the RSS feed is set only if + * {@link #item} is {@code null}. Otherwise, the title of the RSS + * {@link #item} is set. + */ + private final Setter SET_DESCRIPTION = new ContentSetter() { + @Override + public void set(String description) { + if (item == null) { + feed.setDescription(description); + } else { + item.setDescription(description); + } + } + }; + + /** + * Setter for an RSS &lt;content:encoded&gt; element inside an &lt;item&gt; + * element. + */ + private final Setter SET_CONTENT = new ContentSetter() { + @Override + public void set(String content) { + if (item != null) { + item.setContent(content); + } + } + }; + + /** + * Setter for RSS &lt;link&gt; elements inside a &lt;channel&gt; or an + * &lt;item&gt; element. The title of the RSS feed is set only if + * {@link #item} is {@code null}. Otherwise, the title of the RSS + * {@link #item} is set. + */ + private final Setter SET_LINK = new ContentSetter() { + @Override + public void set(String link) { + final android.net.Uri uri = android.net.Uri.parse(link); + if (item == null) { + feed.setLink(uri); + } else { + item.setLink(uri); + } + } + }; + + /** + * Setter for RSS &lt;pubDate&gt; elements inside a &lt;channel&gt; or an + * &lt;item&gt; element. The title of the RSS feed is set only if + * {@link #item} is {@code null}. Otherwise, the title of the RSS + * {@link #item} is set. + */ + private final Setter SET_PUBDATE = new ContentSetter() { + @Override + public void set(String pubDate) { + final java.util.Date date = Dates.parseRfc822(pubDate); + if (item == null) { + feed.setPubDate(date); + } else { + item.setPubDate(date); + } + } + }; + + /** + * Setter for one or multiple RSS &lt;category&gt; elements inside a + * &lt;channel&gt; or an &lt;item&gt; element. The title of the RSS feed is + * set only if {@link #item} is {@code null}. Otherwise, the title of the RSS + * {@link #item} is set. + */ + private final Setter ADD_CATEGORY = new ContentSetter() { + + @Override + public void set(String category) { + if (item == null) { + feed.addCategory(category); + } else { + item.addCategory(category); + } + } + }; + + /** + * Setter for one or multiple RSS &lt;media:thumbnail&gt; elements inside an + * &lt;item&gt; element. The thumbnail element has only attributes. Both its + * height and width are optional. Invalid elements are ignored. + */ + private final Setter ADD_MEDIA_THUMBNAIL = new AttributeSetter() { + + private static final String MEDIA_THUMBNAIL_HEIGHT = "height"; + private static final String MEDIA_THUMBNAIL_WIDTH = "width"; + private static final String MEDIA_THUMBNAIL_URL = "url"; + private static final int DEFAULT_DIMENSION = -1; + + @Override + public void set(org.xml.sax.Attributes attributes) { + if (item == null) { + // ignore invalid media:thumbnail elements which are not inside item + // elements + return; + } + + final int height = MediaAttributes.intValue(attributes, MEDIA_THUMBNAIL_HEIGHT, DEFAULT_DIMENSION); + final int width = MediaAttributes.intValue(attributes, MEDIA_THUMBNAIL_WIDTH, DEFAULT_DIMENSION); + final String url = MediaAttributes.stringValue(attributes, MEDIA_THUMBNAIL_URL); + + if (url == null) { + // ignore invalid media:thumbnail elements which have no URL. + return; + } + + item.addThumbnail(new MediaThumbnail(android.net.Uri.parse(url), height, width)); + } + + }; + + /** + * Use configuration to optimize initial capacities of collections + */ + private final RSSConfig config; + + /** + * Instantiate a SAX handler which can parse a subset of RSS 2.0 feeds. + * + * @param config configuration for the initial capacities of collections + */ + RSSHandler(RSSConfig config) { + this.config = config; + + // initialize dispatchers to manage the state of the SAX handler + setters = new java.util.HashMap<String, Setter>(/* 2^3 */8); + setters.put("title", SET_TITLE); + setters.put("description", SET_DESCRIPTION); + setters.put("content:encoded", SET_CONTENT); + setters.put("link", SET_LINK); + setters.put("category", ADD_CATEGORY); + setters.put("pubDate", SET_PUBDATE); + setters.put("media:thumbnail", ADD_MEDIA_THUMBNAIL); + } + + /** + * Returns the RSS feed after this SAX handler has processed the XML document. + */ + RSSFeed feed() { + return feed; + } + + /** + * Identify the appropriate dispatcher which should be used to store XML data + * in a POJO. Unsupported RSS 2.0 elements are currently ignored. + */ + @Override + public void startElement(String nsURI, String localName, String qname, + org.xml.sax.Attributes attributes) { + // Lookup dispatcher in hash table + setter = setters.get(qname); + if (setter == null) { + if (RSS_ITEM.equals(qname)) { + item = new RSSItem(config.categoryAvg, config.thumbnailAvg); + } + } else if (setter instanceof AttributeSetter) { + ((AttributeSetter) setter).set(attributes); + } else { + // Buffer supported RSS content data + buffer = new StringBuilder(); + } + } + + @Override + public void endElement(String nsURI, String localName, String qname) { + if (isBuffering()) { + // set field of an RSS feed or RSS item + ((ContentSetter) setter).set(buffer.toString()); + + // clear buffer + buffer = null; + } else if (RSS_ITEM.equals(qname)) { + feed.addItem(item); + + // (re)enter <channel> scope + item = null; + } + } + + @Override + public void characters(char ch[], int start, int length) { + if (isBuffering()) { + buffer.append(ch, start, length); + } + } + + /** + * Determines if the SAX parser is ready to receive data inside an XML element + * such as &lt;title&gt; or &lt;description&gt;. + * + * @return boolean {@code true} if the SAX handler parses data inside an XML + * element, {@code false} otherwise + */ + boolean isBuffering() { + return buffer != null && setter != null; + } + +} + diff --git a/src/org/mcsoxford/rss/RSSItem.java b/src/org/mcsoxford/rss/RSSItem.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +/** + * Data about an RSS item. + * + * @author Mr Horn + */ +public class RSSItem extends RSSBase { + private final java.util.List<MediaThumbnail> thumbnails; + private String content; + + /* Internal constructor for RSSHandler */ + RSSItem(byte categoryCapacity, byte thumbnailCapacity) { + super(categoryCapacity); + thumbnails = new java.util.ArrayList<MediaThumbnail>(thumbnailCapacity); + } + + /* Internal method for RSSHandler */ + void addThumbnail(MediaThumbnail thumbnail) { + thumbnails.add(thumbnail); + } + + /** + * Returns an unmodifiable list of thumbnails. The return value is never + * {@code null}. Images are in order of importance. + */ + public java.util.List<MediaThumbnail> getThumbnails() { + return java.util.Collections.unmodifiableList(thumbnails); + } + + /** + * Returns the value of the optional &lt;content:encoded&gt; tag + * @return string value of the element data + */ + public String getContent() { + return content; + } + + /* Internal method for RSSHandler */ + void setContent(String content) { + this.content = content; + } +} + diff --git a/src/org/mcsoxford/rss/RSSLoader.java b/src/org/mcsoxford/rss/RSSLoader.java @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2011 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mcsoxford.rss; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Asynchronous loader for RSS feeds. RSS feeds can be loaded in FIFO order or + * based on priority. Objects of this type can be constructed with one of the + * provided static methods: + * <ul> + * <li>{@link #fifo()}</li> + * <li>{@link #fifo(int)}</li> + * <li>{@link #priority()}</li> + * <li>{@link #priority(int)}</li> + * </ul> + * + * Completed RSS feed loads can be retrieved with {@link RSSLoader#take()}, + * {@link RSSLoader#poll()} or {@link RSSLoader#poll(long, TimeUnit)}. + * + * <p> + * <b>Usage Example</b> + * + * Suppose you want to load an array of RSS feed URIs concurrently before + * retrieving the results one at a time. You could write this as: + * + * <pre> + * {@code + * void fetchRSS(String[] uris) throws InterruptedException { + * RSSLoader loader = RSSLoader.fifo(); + * for (String uri : uris) { + * loader.load(uri); + * } + * + * Future&lt;RSSFeed&gt; future; + * RSSFeed feed; + * for (int i = 0; i &lt; uris.length; i++) { + * future = loader.take(); + * try { + * feed = future.get(); + * use(feed); + * } catch (ExecutionException ignore) {} + * } + * }} + * </pre> + * + * </p> + * + * @author A. Horn + */ +public class RSSLoader { + + /** + * Human-readable name of the thread loading RSS feeds + */ + private final static String DEFAULT_THREAD_NAME = "Asynchronous RSS feed loader"; + + /** + * Arrange incoming load requests on this queue. + */ + private final BlockingQueue<RSSFuture> in; + + /** + * Once the an RSS feed has completed loading, place the result on this queue. + */ + private final BlockingQueue<RSSFuture> out; + + /** + * Flag changes are visible after operations on {@link #in} queue. + */ + private boolean stopped; + + /** + * Create an object which can load RSS feeds asynchronously in FIFO order. + * + * @see #fifo(int) + */ + public static RSSLoader fifo() { + return new RSSLoader(new LinkedBlockingQueue<RSSFuture>()); + } + + /** + * Create an object which can load RSS feeds asynchronously in FIFO order. + * + * @param capacity + * expected number of URIs to be loaded at a given time + */ + public static RSSLoader fifo(int capacity) { + return new RSSLoader(new LinkedBlockingQueue<RSSFuture>(capacity)); + } + + /** + * Create an object which can load RSS feeds asynchronously based on priority. + * + * @see #priority(int) + */ + public static RSSLoader priority() { + return new RSSLoader(new PriorityBlockingQueue<RSSFuture>()); + } + + /** + * Create an object which can load RSS feeds asynchronously based on priority. + * + * @param capacity + * expected number of URIs to be loaded at a given time + */ + public static RSSLoader priority(int capacity) { + return new RSSLoader(new PriorityBlockingQueue<RSSFuture>(capacity)); + } + + /** + * Instantiate an object which can load RSS feeds asynchronously. The provided + * {@link BlockingQueue} implementation determines the load behaviour. + * + * @see LinkedBlockingQueue + * @see PriorityBlockingQueue + */ + RSSLoader(BlockingQueue<RSSFuture> in) { + this.in = in; + this.out = new LinkedBlockingQueue<RSSFuture>(); + + // start separate thread for loading of RSS feeds + new Thread(new Loader(new RSSReader()), DEFAULT_THREAD_NAME).start(); + } + + /** + * Returns {@code true} if RSS feeds are currently being loaded, {@code false} + * otherwise. + */ + public boolean isLoading() { + // order of conjuncts matters because of happens-before relationship + return !in.isEmpty() && !stopped; + } + + /** + * Stop thread after finishing loading pending RSS feed URIs. If this loader + * has been constructed with {@link #priority()} or {@link #priority(int)}, + * only RSS feed loads with priority strictly greater than seven (7) are going + * to be completed. + * <p> + * Subsequent invocations of {@link #load(String)} and + * {@link #load(String, int)} return {@code null}. + */ + public void stop() { + // flag writings happen-before enqueue + stopped = true; + in.offer(SENTINEL); + } + + /** + * Loads the specified RSS feed URI asynchronously. If this loader has been + * constructed with {@link #priority()} or {@link #priority(int)}, then a + * default priority of three (3) is used. Otherwise, RSS feeds are loaded in + * FIFO order. + * <p> + * Returns {@code null} if the RSS feed URI cannot be scheduled for loading + * due to resource constraints or if {@link #stop()} has been previously + * called. + * <p> + * Completed RSS feed loads can be retrieved by calling {@link #take()}. + * Alternatively, non-blocking polling is possible with {@link #poll()}. + * + * @param uri + * RSS feed URI to be loaded + * + * @return Future representing the RSS feed scheduled for loading, + * {@code null} if scheduling failed + */ + public Future<RSSFeed> load(String uri) { + return load(uri, RSSFuture.DEFAULT_PRIORITY); + } + + /** + * Loads the specified RSS feed URI asynchronously. For the specified priority + * to determine the relative loading order of RSS feeds, this loader must have + * been constructed with {@link #priority()} or {@link #priority(int)}. + * Otherwise, RSS feeds are loaded in FIFO order. + * <p> + * Returns {@code null} if the RSS feed URI cannot be scheduled for loading + * due to resource constraints or if {@link #stop()} has been previously + * called. + * <p> + * Completed RSS feed loads can be retrieved by calling {@link #take()}. + * Alternatively, non-blocking polling is possible with {@link #poll()}. + * + * @param uri + * RSS feed URI to be loaded + * @param priority + * larger integer gives higher priority + * + * @return Future representing the RSS feed scheduled for loading, + * {@code null} if scheduling failed + */ + public Future<RSSFeed> load(String uri, int priority) { + if (uri == null) { + throw new IllegalArgumentException("RSS feed URI must not be null."); + } + + // optimization (after flag changes have become visible) + if (stopped) { + return null; + } + + // flag readings happen-after enqueue + final RSSFuture future = new RSSFuture(uri, priority); + final boolean ok = in.offer(future); + + if (!ok || stopped) { + return null; + } + + return future; + } + + /** + * Retrieves and removes the next Future representing the result of loading an + * RSS feed, waiting if none are yet present. + * + * @return the {@link Future} representing the loaded RSS feed + * + * @throws InterruptedException + * if interrupted while waiting + */ + public Future<RSSFeed> take() throws InterruptedException { + return out.take(); + } + + /** + * Retrieves and removes the next Future representing the result of loading an + * RSS feed or {@code null} if none are present. + * + * @return the {@link Future} representing the loaded RSS feed, or + * {@code null} if none are present + * + * @throws InterruptedException + * if interrupted while waiting + */ + public Future<RSSFeed> poll() { + return out.poll(); + } + + /** + * Retrieves and removes the Future representing the result of loading an RSS + * feed, waiting if necessary up to the specified wait time if none are yet + * present. + * + * @param timeout + * how long to wait before giving up, in units of {@code unit} + * @param unit + * a {@link TimeUnit} determining how to interpret the + * {@code timeout} parameter + * @return the {@link Future} representing the loaded RSS feed, or + * {@code null} if none are present within the specified time interval + * @throws InterruptedException + * if interrupted while waiting + */ + public Future<RSSFeed> poll(long timeout, TimeUnit unit) throws InterruptedException { + return out.poll(timeout, unit); + } + + /** + * Internal consumer of RSS feed URIs stored in the blocking queue. + */ + class Loader implements Runnable { + + private final RSSReader reader; + + Loader(RSSReader reader) { + this.reader = reader; + } + + /** + * Keep on loading RSS feeds by dequeuing incoming tasks until the sentinel + * is encountered. + */ + @Override + public void run() { + try { + RSSFuture future = null; + RSSFeed feed; + while ((future = in.take()) != SENTINEL) { + + if (future.status.compareAndSet(RSSFuture.READY, RSSFuture.LOADING)) { + try { + // perform loading outside of locked region + feed = reader.load(future.uri); + + // set successfully loaded RSS feed + future.set(feed, /* error */null); + + // enable caller to consume the loaded RSS feed + out.add(future); + } catch (RSSException e) { + // throw ExecutionException when calling RSSFuture::get() + future.set(/* feed */null, e); + } catch (RSSFault e) { + // throw ExecutionException when calling RSSFuture::get() + future.set(/* feed */null, e); + } finally { + // RSSFuture::isDone() returns true even if an error occurred + future.status.compareAndSet(RSSFuture.LOADING, RSSFuture.LOADED); + } + } + + } + } catch (InterruptedException e) { + // Restore the interrupted status + Thread.currentThread().interrupt(); + } + } + + } + + /** + * Internal sentinel to stop the thread that is loading RSS feeds. + */ + private final static RSSFuture SENTINEL = new RSSFuture(null, /* priority */7); + + /** + * Offer callers control over the asynchronous loading of an RSS feed. + */ + static class RSSFuture implements Future<RSSFeed>, Comparable<RSSFuture> { + + static final int DEFAULT_PRIORITY = 3; + static final int READY = 0; + static final int LOADING = 1; + static final int LOADED = 2; + static final int CANCELLED = 4; + + /** RSS feed URI */ + final String uri; + + /** Larger integer gives higher priority */ + final int priority; + + AtomicInteger status; + + boolean waiting; + RSSFeed feed; + Exception cause; + + RSSFuture(String uri, int priority) { + this.uri = uri; + this.priority = priority; + status = new AtomicInteger(READY); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return isCancelled() || status.compareAndSet(READY, CANCELLED); + } + + @Override + public boolean isCancelled() { + return status.get() == CANCELLED; + } + + @Override + public boolean isDone() { + return (status.get() & (LOADED | CANCELLED)) != 0; + } + + @Override + public synchronized RSSFeed get() throws InterruptedException, ExecutionException { + if (feed == null && cause == null) { + try { + waiting = true; + + // guard against spurious wakeups + while (waiting) { + wait(); + } + } finally { + waiting = false; + } + } + + if (cause != null) { + throw new ExecutionException(cause); + } + + return feed; + } + + @Override + public synchronized RSSFeed get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + + if (feed == null && cause == null) { + try { + waiting = true; + + final long timeoutMillis = unit.toMillis(timeout); + final long startMillis = System.currentTimeMillis(); + + // guard against spurious wakeups + while (waiting) { + wait(timeoutMillis); + + // check timeout + if (System.currentTimeMillis() - startMillis > timeoutMillis) { + throw new TimeoutException("RSS feed loading timed out"); + } + } + } finally { + waiting = false; + } + } + + if (cause != null) { + throw new ExecutionException(cause); + } + + return feed; + } + + synchronized void set(RSSFeed feed, Exception cause) { + this.feed = feed; + this.cause = cause; + + if (waiting) { + waiting = false; + notifyAll(); + } + } + + @Override + public int compareTo(RSSFuture other) { + // Note: head of PriorityQueue implementation is the least element + return other.priority - priority; + } + } + +} + diff --git a/src/org/mcsoxford/rss/RSSParser.java b/src/org/mcsoxford/rss/RSSParser.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +import java.io.IOException; +import java.io.InputStream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + +/** + * Internal thread-safe RSS parser SPI implementation. + * + * @author Mr Horn + */ +class RSSParser implements RSSParserSPI { + + private final RSSConfig config; + + /* Internal constructor for RSSReader */ + RSSParser(RSSConfig config) { + this.config = config; + } + + /** + * Parses input stream as RSS feed. It is the responsibility of the caller to + * close the RSS feed input stream. + * + * @param feed RSS 2.0 feed input stream + * @return in-memory representation of RSS feed + * @throws RSSFault if an unrecoverable parse error occurs + */ + @Override + public RSSFeed parse(InputStream feed) { + try { + // Since SAXParserFactory implementations are not guaranteed to be + // thread-safe, a new local object is instantiated. + final SAXParserFactory factory = SAXParserFactory.newInstance(); + + // Support Android 1.6 (see Issue 1) + factory.setFeature("http://xml.org/sax/features/namespaces", false); + factory.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + + final SAXParser parser = factory.newSAXParser(); + + return parse(parser, feed); + } catch (ParserConfigurationException e) { + throw new RSSFault(e); + } catch (SAXException e) { + throw new RSSFault(e); + } catch (IOException e) { + throw new RSSFault(e); + } + } + + /** + * Parses input stream as an RSS 2.0 feed. + * + * @return in-memory representation of an RSS feed + * @throws IllegalArgumentException if either argument is {@code null} + */ + private RSSFeed parse(SAXParser parser, InputStream feed) + throws SAXException, IOException { + if (parser == null) { + throw new IllegalArgumentException("RSS parser must not be null."); + } else if (feed == null) { + throw new IllegalArgumentException("RSS feed must not be null."); + } + + // SAX automatically detects the correct character encoding from the stream + // See also http://www.w3.org/TR/REC-xml/#sec-guessing + final InputSource source = new InputSource(feed); + final XMLReader xmlreader = parser.getXMLReader(); + final RSSHandler handler = new RSSHandler(config); + + xmlreader.setContentHandler(handler); + xmlreader.parse(source); + + return handler.feed(); + } + +} + diff --git a/src/org/mcsoxford/rss/RSSParserSPI.java b/src/org/mcsoxford/rss/RSSParserSPI.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +/** + * Thread-safe RSS parser service provider interface. + * + * @author Mr Horn + */ +public interface RSSParserSPI { + + /** + * Parses an input stream as an RSS feed. It is the responsibility of the + * caller to close the specified RSS feed input stream. + * + * @param feed RSS 2.0 feed input stream + * @return in-memory representation of RSS feed + * @throws RSSFault if an unrecoverable parse error occurs + */ + RSSFeed parse(java.io.InputStream feed); + +} + diff --git a/src/org/mcsoxford/rss/RSSReader.java b/src/org/mcsoxford/rss/RSSReader.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; + +/** + * HTTP client to retrieve and parse RSS 2.0 feeds. Callers must call + * {@link RSSReader#close()} to release all resources. + * + * @author Mr Horn + */ +public class RSSReader implements java.io.Closeable { + + /** + * Thread-safe {@link HttpClient} implementation. + */ + private final HttpClient httpclient; + + /** + * Thread-safe RSS parser SPI. + */ + private final RSSParserSPI parser; + + /** + * Instantiate a thread-safe HTTP client to retrieve RSS feeds. The injected + * {@link HttpClient} implementation must be thread-safe. + * + * @param httpclient thread-safe HTTP client implementation + * @param parser thread-safe RSS parser SPI implementation + */ + public RSSReader(HttpClient httpclient, RSSParserSPI parser) { + this.httpclient = httpclient; + this.parser = parser; + } + + /** + * Instantiate a thread-safe HTTP client to retrieve RSS feeds. The injected + * {@link HttpClient} implementation must be thread-safe. Internal memory + * consumption and load performance can be tweaked with {@link RSSConfig}. + * + * @param httpclient thread-safe HTTP client implementation + * @param config RSS configuration + */ + public RSSReader(HttpClient httpclient, RSSConfig config) { + this(httpclient, new RSSParser(config)); + } + + /** + * Instantiate a thread-safe HTTP client to retrieve and parse RSS feeds. + * Internal memory consumption and load performance can be tweaked with + * {@link RSSConfig}. + */ + public RSSReader(RSSConfig config) { + this(new DefaultHttpClient(), new RSSParser(config)); + } + + /** + * Instantiate a thread-safe HTTP client to retrieve and parse RSS feeds. + * Default RSS configuration capacity values are used. + */ + public RSSReader() { + this(new DefaultHttpClient(), new RSSParser(new RSSConfig())); + } + + /** + * Send HTTP GET request and parse the XML response to construct an in-memory + * representation of an RSS 2.0 feed. + * + * @param uri RSS 2.0 feed URI + * @return in-memory representation of downloaded RSS feed + * @throws RSSReaderException if RSS feed could not be retrieved because of + * HTTP error + * @throws RSSFault if an unrecoverable IO error has occurred + */ + public RSSFeed load(String uri) throws RSSReaderException { + final HttpGet httpget = new HttpGet(uri); + + InputStream feedStream = null; + try { + // Send GET request to URI + final HttpResponse response = httpclient.execute(httpget); + + // Check if server response is valid + final StatusLine status = response.getStatusLine(); + if (status.getStatusCode() != HttpStatus.SC_OK) { + throw new RSSReaderException(status.getStatusCode(), + status.getReasonPhrase()); + } + + // Extract content stream from HTTP response + HttpEntity entity = response.getEntity(); + feedStream = entity.getContent(); + + RSSFeed feed = parser.parse(feedStream); + + if (feed.getLink() == null) { + feed.setLink(android.net.Uri.parse(uri)); + } + + return feed; + } catch (ClientProtocolException e) { + throw new RSSFault(e); + } catch (IOException e) { + throw new RSSFault(e); + } finally { + Resources.closeQuietly(feedStream); + } + } + + /** + * Release all HTTP client resources. + */ + public void close() { + httpclient.getConnectionManager().shutdown(); + } + +} + diff --git a/src/org/mcsoxford/rss/RSSReaderException.java b/src/org/mcsoxford/rss/RSSReaderException.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +/** + * Contingency exception raised when RSS feeds could not be retrieved. + * + * @author Mr Horn + */ +public class RSSReaderException extends RSSException { + + /** + * Unsupported serialization + */ + private static final long serialVersionUID = 1L; + + private final int status; + + public RSSReaderException(int status, String message) { + super(message); + this.status = status; + } + + public RSSReaderException(int status, Throwable cause) { + super(cause); + this.status = status; + } + + public RSSReaderException(int status, String message, Throwable cause) { + super(message, cause); + this.status = status; + } + + /** + * Return the HTTP status which caused the error. + * + * @return HTTP error status code + */ + public int getStatus() { + return status; + } +} + diff --git a/src/org/mcsoxford/rss/Resources.java b/src/org/mcsoxford/rss/Resources.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010 A. Horn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mcsoxford.rss; + +/** + * Internal helper class for resource-sensitive objects such as streams. + * + * @author Mr Horn + */ +final class Resources { + + /* Hide constructor */ + private Resources() {} + + /** + * Closes stream and suppresses IO faults. + * + * @return {@code null} if stream has been successfully closed, + * {@link java.io.IOException} otherwise + */ + static java.io.IOException closeQuietly(java.io.Closeable stream) { + if (stream == null) { + return null; + } + + try { + stream.close(); + } catch (java.io.IOException e) { + return e; + } + + return null; + } + +} + diff --git a/src/org/xmlrpc/android/Base64Coder.java b/src/org/xmlrpc/android/Base64Coder.java @@ -0,0 +1,199 @@ +package org.xmlrpc.android; + +/** + * A Base64 Encoder/Decoder. + * + * <p> + * This class is used to encode and decode data in Base64 format as described in + * RFC 1521. + * + * <p> + * This is "Open Source" software and released under the <a + * href="http://www.gnu.org/licenses/lgpl.html">GNU/LGPL</a> license.<br> + * It is provided "as is" without warranty of any kind.<br> + * Copyright 2003: Christian d'Heureuse, Inventec Informatik AG, Switzerland.<br> + * Home page: <a href="http://www.source-code.biz">www.source-code.biz</a><br> + * + * <p> + * Version history:<br> + * 2003-07-22 Christian d'Heureuse (chdh): Module created.<br> + * 2005-08-11 chdh: Lincense changed from GPL to LGPL.<br> + * 2006-11-21 chdh:<br> + * &nbsp; Method encode(String) renamed to encodeString(String).<br> + * &nbsp; Method decode(String) renamed to decodeString(String).<br> + * &nbsp; New method encode(byte[],int) added.<br> + * &nbsp; New method decode(String) added.<br> + */ + +class Base64Coder { + + // Mapping table from 6-bit nibbles to Base64 characters. + private static char[] map1 = new char[64]; + static { + int i = 0; + for (char c = 'A'; c <= 'Z'; c++) { + map1[i++] = c; + } + for (char c = 'a'; c <= 'z'; c++) { + map1[i++] = c; + } + for (char c = '0'; c <= '9'; c++) { + map1[i++] = c; + } + map1[i++] = '+'; + map1[i++] = '/'; + } + + // Mapping table from Base64 characters to 6-bit nibbles. + private static byte[] map2 = new byte[128]; + static { + for (int i = 0; i < map2.length; i++) { + map2[i] = -1; + } + for (int i = 0; i < 64; i++) { + map2[map1[i]] = (byte) i; + } + } + + /** + * Encodes a string into Base64 format. No blanks or line breaks are + * inserted. + * + * @param s + * a String to be encoded. + * @return A String with the Base64 encoded data. + */ + static String encodeString(String s) { + return new String(encode(s.getBytes())); + } + + /** + * Encodes a byte array into Base64 format. No blanks or line breaks are + * inserted. + * + * @param in + * an array containing the data bytes to be encoded. + * @return A character array with the Base64 encoded data. + */ + static char[] encode(byte[] in) { + return encode(in, in.length); + } + + /** + * Encodes a byte array into Base64 format. No blanks or line breaks are + * inserted. + * + * @param in + * an array containing the data bytes to be encoded. + * @param iLen + * number of bytes to process in <code>in</code>. + * @return A character array with the Base64 encoded data. + */ + static char[] encode(byte[] in, int iLen) { + int oDataLen = (iLen * 4 + 2) / 3; // output length without padding + int oLen = ((iLen + 2) / 3) * 4; // output length including padding + char[] out = new char[oLen]; + int ip = 0; + int op = 0; + while (ip < iLen) { + int i0 = in[ip++] & 0xff; + int i1 = ip < iLen ? in[ip++] & 0xff : 0; + int i2 = ip < iLen ? in[ip++] & 0xff : 0; + int o0 = i0 >>> 2; + int o1 = ((i0 & 3) << 4) | (i1 >>> 4); + int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); + int o3 = i2 & 0x3F; + out[op++] = map1[o0]; + out[op++] = map1[o1]; + out[op] = op < oDataLen ? map1[o2] : '='; + op++; + out[op] = op < oDataLen ? map1[o3] : '='; + op++; + } + return out; + } + + /** + * Decodes a string from Base64 format. + * + * @param s + * a Base64 String to be decoded. + * @return A String containing the decoded data. + * @throws IllegalArgumentException + * if the input is not valid Base64 encoded data. + */ + static String decodeString(String s) { + return new String(decode(s)); + } + + /** + * Decodes a byte array from Base64 format. + * + * @param s + * a Base64 String to be decoded. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException + * if the input is not valid Base64 encoded data. + */ + static byte[] decode(String s) { + return decode(s.toCharArray()); + } + + /** + * Decodes a byte array from Base64 format. No blanks or line breaks are + * allowed within the Base64 encoded data. + * + * @param in + * a character array containing the Base64 encoded data. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException + * if the input is not valid Base64 encoded data. + */ + static byte[] decode(char[] in) { + int iLen = in.length; + if (iLen % 4 != 0) { + throw new IllegalArgumentException( + "Length of Base64 encoded input string is not a multiple of 4."); + } + while (iLen > 0 && in[iLen - 1] == '=') { + iLen--; + } + int oLen = (iLen * 3) / 4; + byte[] out = new byte[oLen]; + int ip = 0; + int op = 0; + while (ip < iLen) { + int i0 = in[ip++]; + int i1 = in[ip++]; + int i2 = ip < iLen ? in[ip++] : 'A'; + int i3 = ip < iLen ? in[ip++] : 'A'; + if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) { + throw new IllegalArgumentException( + "Illegal character in Base64 encoded data."); + } + int b0 = map2[i0]; + int b1 = map2[i1]; + int b2 = map2[i2]; + int b3 = map2[i3]; + if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) { + throw new IllegalArgumentException( + "Illegal character in Base64 encoded data."); + } + int o0 = (b0 << 2) | (b1 >>> 4); + int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); + int o2 = ((b2 & 3) << 6) | b3; + out[op++] = (byte) o0; + if (op < oLen) { + out[op++] = (byte) o1; + } + if (op < oLen) { + out[op++] = (byte) o2; + } + } + return out; + } + + // Dummy constructor. + private Base64Coder() { + } +} diff --git a/src/org/xmlrpc/android/IXMLRPCSerializer.java b/src/org/xmlrpc/android/IXMLRPCSerializer.java @@ -0,0 +1,32 @@ +package org.xmlrpc.android; + +import java.io.IOException; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +public interface IXMLRPCSerializer { + String TAG_NAME = "name"; + String TAG_MEMBER = "member"; + String TAG_VALUE = "value"; + String TAG_DATA = "data"; + + String TYPE_INT = "int"; + String TYPE_I4 = "i4"; + String TYPE_I8 = "i8"; + String TYPE_DOUBLE = "double"; + String TYPE_BOOLEAN = "boolean"; + String TYPE_STRING = "string"; + String TYPE_DATE_TIME_ISO8601 = "dateTime.iso8601"; + String TYPE_BASE64 = "base64"; + String TYPE_ARRAY = "array"; + String TYPE_STRUCT = "struct"; + // This added by mattias.ellback as part of issue #19 + String TYPE_NULL = "nil"; + + String DATETIME_FORMAT = "yyyyMMdd'T'HH:mm:ss"; + + void serialize(XmlSerializer serializer, Object object) throws IOException; + Object deserialize(XmlPullParser parser) throws XmlPullParserException, IOException; +} diff --git a/src/org/xmlrpc/android/MethodCall.java b/src/org/xmlrpc/android/MethodCall.java @@ -0,0 +1,20 @@ +package org.xmlrpc.android; + +import java.util.ArrayList; + +public class MethodCall { + + private static final int TOPIC = 1; + String methodName; + ArrayList<Object> params = new ArrayList<Object>(); + + public String getMethodName() { return methodName; } + void setMethodName(String methodName) { this.methodName = methodName; } + + public ArrayList<Object> getParams() { return params; } + void setParams(ArrayList<Object> params) { this.params = params; } + + public String getTopic() { + return (String)params.get(TOPIC); + } +} diff --git a/src/org/xmlrpc/android/Tag.java b/src/org/xmlrpc/android/Tag.java @@ -0,0 +1,13 @@ +package org.xmlrpc.android; + +public class Tag { + static final String LOG = "XMLRPC"; + static final String METHOD_CALL = "methodCall"; + static final String METHOD_NAME = "methodName"; + static final String METHOD_RESPONSE = "methodResponse"; + static final String PARAMS = "params"; + static final String PARAM = "param"; + static final String FAULT = "fault"; + static final String FAULT_CODE = "faultCode"; + static final String FAULT_STRING = "faultString"; +}+ \ No newline at end of file diff --git a/src/org/xmlrpc/android/XMLRPCClient.java b/src/org/xmlrpc/android/XMLRPCClient.java @@ -0,0 +1,559 @@ +package org.xmlrpc.android; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +import java.net.URI; +import java.net.URL; +import java.util.Map; +import java.util.Vector; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserFactory; + +/** + * XMLRPCClient allows to call remote XMLRPC method. + * + * <p> + * The following table shows how XML-RPC types are mapped to java call parameters/response values. + * </p> + * + * <p> + * <table border="2" align="center" cellpadding="5"> + * <thead><tr><th>XML-RPC Type</th><th>Call Parameters</th><th>Call Response</th></tr></thead> + * + * <tbody> + * <td>int, i4</td><td>byte<br />Byte<br />short<br />Short<br />int<br />Integer</td><td>int<br />Integer</td> + * </tr> + * <tr> + * <td>i8</td><td>long<br />Long</td><td>long<br />Long</td> + * </tr> + * <tr> + * <td>double</td><td>float<br />Float<br />double<br />Double</td><td>double<br />Double</td> + * </tr> + * <tr> + * <td>string</td><td>String</td><td>String</td> + * </tr> + * <tr> + * <td>boolean</td><td>boolean<br />Boolean</td><td>boolean<br />Boolean</td> + * </tr> + * <tr> + * <td>dateTime.iso8601</td><td>java.util.Date<br />java.util.Calendar</td><td>java.util.Date</td> + * </tr> + * <tr> + * <td>base64</td><td>byte[]</td><td>byte[]</td> + * </tr> + * <tr> + * <td>array</td><td>java.util.List&lt;Object&gt;<br />Object[]</td><td>Object[]</td> + * </tr> + * <tr> + * <td>struct</td><td>java.util.Map&lt;String, Object&gt;</td><td>java.util.Map&lt;String, Object&gt;</td> + * </tr> + * </tbody> + * </table> + * </p> + * <p> + * You can also pass as a parameter any object implementing XMLRPCSerializable interface. In this + * case your object overrides getSerializable() telling how to serialize to XMLRPC protocol + * </p> + */ + +public class XMLRPCClient extends XMLRPCCommon { + private HttpClient client; + private HttpPost postMethod; + private HttpParams httpParams; + // These variables used in the code inspired by erickok in issue #6 + private boolean httpPreAuth = false; + private String username = ""; + private String password = ""; + + /** + * XMLRPCClient constructor. Creates new instance based on server URI + * (Code contributed by sgayda2 from issue #17, and by erickok from ticket #10) + * + * @param XMLRPC server URI + */ + public XMLRPCClient(URI uri) { + SchemeRegistry registry = new SchemeRegistry(); + registry.register(new Scheme("http", new PlainSocketFactory(), 80)); + registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443)); + + postMethod = new HttpPost(uri); + postMethod.addHeader("Content-Type", "text/xml"); + + // WARNING + // I had to disable "Expect: 100-Continue" header since I had + // two second delay between sending http POST request and POST body + httpParams = postMethod.getParams(); + HttpProtocolParams.setUseExpectContinue(httpParams, false); + this.client = new DefaultHttpClient(new ThreadSafeClientConnManager(httpParams, registry), httpParams); + } + + /** + * XMLRPCClient constructor. Creates new instance based on server URI + * (Code contributed by sgayda2 from issue #17) + * + * @param XMLRPC server URI + * @param HttpClient to use + */ + + public XMLRPCClient(URI uri, HttpClient client) { + postMethod = new HttpPost(uri); + postMethod.addHeader("Content-Type", "text/xml"); + + // WARNING + // I had to disable "Expect: 100-Continue" header since I had + // two second delay between sending http POST request and POST body + httpParams = postMethod.getParams(); + HttpProtocolParams.setUseExpectContinue(httpParams, false); + this.client = client; + } + + /** + * Amends user agent + * (Code contributed by mortenholdflod from issue #28) + * + * @param userAgent defining the new User Agent string + */ + public void setUserAgent(String userAgent) { + postMethod.removeHeaders("User-Agent"); + postMethod.addHeader("User-Agent", userAgent); + } + + /** + * Convenience constructor. Creates new instance based on server String address + * @param XMLRPC server address + */ + public XMLRPCClient(String url) { + this(URI.create(url)); + } + + /** + * Convenience constructor. Creates new instance based on server String address + * @param XMLRPC server address + * @param HttpClient to use + */ + public XMLRPCClient(String url, HttpClient client) { + this(URI.create(url), client); + } + + + /** + * Convenience XMLRPCClient constructor. Creates new instance based on server URL + * @param XMLRPC server URL + */ + public XMLRPCClient(URL url) { + this(URI.create(url.toExternalForm())); + } + + /** + * Convenience XMLRPCClient constructor. Creates new instance based on server URL + * @param XMLRPC server URL + * @param HttpClient to use + */ + public XMLRPCClient(URL url, HttpClient client) { + this(URI.create(url.toExternalForm()), client); + } + + /** + * Convenience constructor. Creates new instance based on server String address + * @param XMLRPC server address + * @param HTTP Server - Basic Authentication - Username + * @param HTTP Server - Basic Authentication - Password + */ + public XMLRPCClient(URI uri, String username, String password) { + this(uri); + + ((DefaultHttpClient) client).getCredentialsProvider().setCredentials( + new AuthScope(uri.getHost(), uri.getPort(),AuthScope.ANY_REALM), + new UsernamePasswordCredentials(username, password)); + } + + /** + * Convenience constructor. Creates new instance based on server String address + * @param XMLRPC server address + * @param HTTP Server - Basic Authentication - Username + * @param HTTP Server - Basic Authentication - Password + * @param HttpClient to use + */ + public XMLRPCClient(URI uri, String username, String password, HttpClient client) { + this(uri, client); + + ((DefaultHttpClient) this.client).getCredentialsProvider().setCredentials( + new AuthScope(uri.getHost(), uri.getPort(),AuthScope.ANY_REALM), + new UsernamePasswordCredentials(username, password)); + } + + /** + * Convenience constructor. Creates new instance based on server String address + * @param XMLRPC server address + * @param HTTP Server - Basic Authentication - Username + * @param HTTP Server - Basic Authentication - Password + */ + public XMLRPCClient(String url, String username, String password) { + this(URI.create(url), username, password); + } + + /** + * Convenience constructor. Creates new instance based on server String address + * @param XMLRPC server address + * @param HTTP Server - Basic Authentication - Username + * @param HTTP Server - Basic Authentication - Password + * @param HttpClient to use + */ + public XMLRPCClient(String url, String username, String password, HttpClient client) { + this(URI.create(url), username, password, client); + } + + /** + * Convenience constructor. Creates new instance based on server String address + * @param XMLRPC server url + * @param HTTP Server - Basic Authentication - Username + * @param HTTP Server - Basic Authentication - Password + */ + public XMLRPCClient(URL url, String username, String password) { + this(URI.create(url.toExternalForm()), username, password); + } + + /** + * Convenience constructor. Creates new instance based on server String address + * @param XMLRPC server url + * @param HTTP Server - Basic Authentication - Username + * @param HTTP Server - Basic Authentication - Password + * @param HttpClient to use + */ + public XMLRPCClient(URL url, String username, String password, HttpClient client) { + this(URI.create(url.toExternalForm()), username, password, client); + } + + /** + * Sets basic authentication on web request using plain credentials + * @param username The plain text username + * @param password The plain text password + * @param doPreemptiveAuth Select here whether to authenticate without it being requested first by the server. + */ + public void setBasicAuthentication(String username, String password, boolean doPreemptiveAuth) { + // This code required to trigger the patch created by erickok in issue #6 + if(doPreemptiveAuth = true) { + this.httpPreAuth = doPreemptiveAuth; + this.username = username; + this.password = password; + } else { + ((DefaultHttpClient) client).getCredentialsProvider().setCredentials(new AuthScope(postMethod.getURI().getHost(), postMethod.getURI().getPort(), AuthScope.ANY_REALM), new UsernamePasswordCredentials(username, password)); + } + } + + /** + * Convenience Constructor: Sets basic authentication on web request using plain credentials + * @param username The plain text username + * @param password The plain text password + */ + public void setBasicAuthentication(String username, String password) { + setBasicAuthentication(username, password, false); + } + + /** + * Call method with optional parameters. This is general method. + * If you want to call your method with 0-8 parameters, you can use more + * convenience call() methods + * + * @param method name of method to call + * @param params parameters to pass to method (may be null if method has no parameters) + * @return deserialized method return value + * @throws XMLRPCException + */ + @SuppressWarnings("unchecked") + public Object callEx(String method, Object[] params) throws XMLRPCException { + try { + // prepare POST body + String body = methodCall(method, params); + + // set POST body + HttpEntity entity = new StringEntity(body); + postMethod.setEntity(entity); + + // This code slightly tweaked from the code by erickok in issue #6 + // Force preemptive authentication + // This makes sure there is an 'Authentication: ' header being send before trying and failing and retrying + // by the basic authentication mechanism of DefaultHttpClient + if(this.httpPreAuth == true) { + String auth = this.username + ":" + this.password; + postMethod.addHeader("Authorization", "Basic " + Base64Coder.encode(auth.getBytes()).toString()); + } + + //Log.d(Tag.LOG, "ros HTTP POST"); + // execute HTTP POST request + HttpResponse response = client.execute(postMethod); + //Log.d(Tag.LOG, "ros HTTP POSTed"); + + // check status code + int statusCode = response.getStatusLine().getStatusCode(); + //Log.d(Tag.LOG, "ros status code:" + statusCode); + if (statusCode != HttpStatus.SC_OK) { + throw new XMLRPCException("HTTP status code: " + statusCode + " != " + HttpStatus.SC_OK, statusCode); + } + + // parse response stuff + // + // setup pull parser + XmlPullParser pullParser = XmlPullParserFactory.newInstance().newPullParser(); + entity = response.getEntity(); + Reader reader = new InputStreamReader(new BufferedInputStream(entity.getContent())); +// for testing purposes only +// reader = new StringReader("<?xml version='1.0'?><methodResponse><params><param><value>\n\n\n</value></param></params></methodResponse>"); + pullParser.setInput(reader); + + // lets start pulling... + pullParser.nextTag(); + pullParser.require(XmlPullParser.START_TAG, null, Tag.METHOD_RESPONSE); + + pullParser.nextTag(); // either Tag.PARAMS (<params>) or Tag.FAULT (<fault>) + String tag = pullParser.getName(); + if (tag.equals(Tag.PARAMS)) { + // normal response + pullParser.nextTag(); // Tag.PARAM (<param>) + pullParser.require(XmlPullParser.START_TAG, null, Tag.PARAM); + pullParser.nextTag(); // Tag.VALUE (<value>) + // no parser.require() here since its called in XMLRPCSerializer.deserialize() below + + // deserialize result + Object obj = iXMLRPCSerializer.deserialize(pullParser); + entity.consumeContent(); + return obj; + } else + if (tag.equals(Tag.FAULT)) { + // fault response + pullParser.nextTag(); // Tag.VALUE (<value>) + // no parser.require() here since its called in XMLRPCSerializer.deserialize() below + + // deserialize fault result + Map<String, Object> map = (Map<String, Object>) iXMLRPCSerializer.deserialize(pullParser); + String faultString = (String) map.get(Tag.FAULT_STRING); + int faultCode = (Integer) map.get(Tag.FAULT_CODE); + entity.consumeContent(); + throw new XMLRPCFault(faultString, faultCode); + } else { + entity.consumeContent(); + throw new XMLRPCException("Bad tag <" + tag + "> in XMLRPC response - neither <params> nor <fault>"); + } + } catch (XMLRPCException e) { + // catch & propagate XMLRPCException/XMLRPCFault + throw e; + } catch (Exception e) { + e.printStackTrace(); + // wrap any other Exception(s) around XMLRPCException + throw new XMLRPCException(e); + } + } + + private String methodCall(String method, Object[] params) + throws IllegalArgumentException, IllegalStateException, IOException { + StringWriter bodyWriter = new StringWriter(); + serializer.setOutput(bodyWriter); + serializer.startDocument(null, null); + serializer.startTag(null, Tag.METHOD_CALL); + // set method name + serializer.startTag(null, Tag.METHOD_NAME).text(method).endTag(null, Tag.METHOD_NAME); + + serializeParams(params); + + serializer.endTag(null, Tag.METHOD_CALL); + serializer.endDocument(); + + return bodyWriter.toString(); + } + + /** + * Convenience method call with no parameters + * + * @param method name of method to call + * @return deserialized method return value + * @throws XMLRPCException + */ + public Object call(String method) throws XMLRPCException { + return callEx(method, null); + } + + /** + * Convenience method call with a vectorized parameter + * (Code contributed by jahbromo from issue #14) + * @param method name of method to call + * @param paramsv vector of method's parameter + * @return deserialized method return value + * @throws XMLRPCException + */ + + public Object call(String method, Vector paramsv) throws XMLRPCException { + Object[] params = new Object [paramsv.size()]; + for (int i=0; i<paramsv.size(); i++) { + params[i]=paramsv.elementAt(i); + } + return callEx(method, params); + } + + /** + * Convenience method call with one parameter + * + * @param method name of method to call + * @param p0 method's parameter + * @return deserialized method return value + * @throws XMLRPCException + */ + public Object call(String method, Object p0) throws XMLRPCException { + Object[] params = { + p0, + }; + return callEx(method, params); + } + + /** + * Convenience method call with two parameters + * + * @param method name of method to call + * @param p0 method's 1st parameter + * @param p1 method's 2nd parameter + * @return deserialized method return value + * @throws XMLRPCException + */ + public Object call(String method, Object p0, Object p1) throws XMLRPCException { + Object[] params = { + p0, p1, + }; + return callEx(method, params); + } + + /** + * Convenience method call with three parameters + * + * @param method name of method to call + * @param p0 method's 1st parameter + * @param p1 method's 2nd parameter + * @param p2 method's 3rd parameter + * @return deserialized method return value + * @throws XMLRPCException + */ + public Object call(String method, Object p0, Object p1, Object p2) throws XMLRPCException { + Object[] params = { + p0, p1, p2, + }; + return callEx(method, params); + } + + /** + * Convenience method call with four parameters + * + * @param method name of method to call + * @param p0 method's 1st parameter + * @param p1 method's 2nd parameter + * @param p2 method's 3rd parameter + * @param p3 method's 4th parameter + * @return deserialized method return value + * @throws XMLRPCException + */ + public Object call(String method, Object p0, Object p1, Object p2, Object p3) throws XMLRPCException { + Object[] params = { + p0, p1, p2, p3, + }; + return callEx(method, params); + } + + /** + * Convenience method call with five parameters + * + * @param method name of method to call + * @param p0 method's 1st parameter + * @param p1 method's 2nd parameter + * @param p2 method's 3rd parameter + * @param p3 method's 4th parameter + * @param p4 method's 5th parameter + * @return deserialized method return value + * @throws XMLRPCException + */ + public Object call(String method, Object p0, Object p1, Object p2, Object p3, Object p4) throws XMLRPCException { + Object[] params = { + p0, p1, p2, p3, p4, + }; + return callEx(method, params); + } + + /** + * Convenience method call with six parameters + * + * @param method name of method to call + * @param p0 method's 1st parameter + * @param p1 method's 2nd parameter + * @param p2 method's 3rd parameter + * @param p3 method's 4th parameter + * @param p4 method's 5th parameter + * @param p5 method's 6th parameter + * @return deserialized method return value + * @throws XMLRPCException + */ + public Object call(String method, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5) throws XMLRPCException { + Object[] params = { + p0, p1, p2, p3, p4, p5, + }; + return callEx(method, params); + } + + /** + * Convenience method call with seven parameters + * + * @param method name of method to call + * @param p0 method's 1st parameter + * @param p1 method's 2nd parameter + * @param p2 method's 3rd parameter + * @param p3 method's 4th parameter + * @param p4 method's 5th parameter + * @param p5 method's 6th parameter + * @param p6 method's 7th parameter + * @return deserialized method return value + * @throws XMLRPCException + */ + public Object call(String method, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6) throws XMLRPCException { + Object[] params = { + p0, p1, p2, p3, p4, p5, p6, + }; + return callEx(method, params); + } + + /** + * Convenience method call with eight parameters + * + * @param method name of method to call + * @param p0 method's 1st parameter + * @param p1 method's 2nd parameter + * @param p2 method's 3rd parameter + * @param p3 method's 4th parameter + * @param p4 method's 5th parameter + * @param p5 method's 6th parameter + * @param p6 method's 7th parameter + * @param p7 method's 8th parameter + * @return deserialized method return value + * @throws XMLRPCException + */ + public Object call(String method, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7) throws XMLRPCException { + Object[] params = { + p0, p1, p2, p3, p4, p5, p6, p7, + }; + return callEx(method, params); + } +} diff --git a/src/org/xmlrpc/android/XMLRPCCommon.java b/src/org/xmlrpc/android/XMLRPCCommon.java @@ -0,0 +1,43 @@ +package org.xmlrpc.android; + +import java.io.IOException; + +import org.xmlpull.v1.XmlSerializer; + +import android.util.Xml; + +class XMLRPCCommon { + + protected XmlSerializer serializer; + protected IXMLRPCSerializer iXMLRPCSerializer; + + XMLRPCCommon() { + serializer = Xml.newSerializer(); + iXMLRPCSerializer = new XMLRPCSerializer(); + } + + /** + * Sets custom IXMLRPCSerializer serializer (in case when server doesn't support + * standard XMLRPC protocol) + * + * @param serializer custom serializer + */ + public void setSerializer(IXMLRPCSerializer serializer) { + iXMLRPCSerializer = serializer; + } + + protected void serializeParams(Object[] params) throws IllegalArgumentException, IllegalStateException, IOException { + if (params != null && params.length != 0) + { + // set method params + serializer.startTag(null, Tag.PARAMS); + for (int i=0; i<params.length; i++) { + serializer.startTag(null, Tag.PARAM).startTag(null, IXMLRPCSerializer.TAG_VALUE); + iXMLRPCSerializer.serialize(serializer, params[i]); + serializer.endTag(null, IXMLRPCSerializer.TAG_VALUE).endTag(null, Tag.PARAM); + } + serializer.endTag(null, Tag.PARAMS); + } + } + +} diff --git a/src/org/xmlrpc/android/XMLRPCException.java b/src/org/xmlrpc/android/XMLRPCException.java @@ -0,0 +1,30 @@ +package org.xmlrpc.android; + +import org.apache.http.HttpStatus; + +public class XMLRPCException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 7499675036625522379L; + + private int httpStatusCode = HttpStatus.SC_OK; + + public XMLRPCException(Exception e) { + super(e); + } + + public XMLRPCException(String string) { + super(string); + } + + public XMLRPCException(String string, int httpStatusCode) { + this(string); + this.httpStatusCode = httpStatusCode; + } + + public int getHttpStatusCode() { + return httpStatusCode; + } +} diff --git a/src/org/xmlrpc/android/XMLRPCFault.java b/src/org/xmlrpc/android/XMLRPCFault.java @@ -0,0 +1,24 @@ +package org.xmlrpc.android; + +public class XMLRPCFault extends XMLRPCException { + /** + * + */ + private static final long serialVersionUID = 5676562456612956519L; + private String faultString; + private int faultCode; + + public XMLRPCFault(String faultString, int faultCode) { + super("XMLRPC Fault: " + faultString + " [code " + faultCode + "]"); + this.faultString = faultString; + this.faultCode = faultCode; + } + + public String getFaultString() { + return faultString; + } + + public int getFaultCode() { + return faultCode; + } +} diff --git a/src/org/xmlrpc/android/XMLRPCSerializable.java b/src/org/xmlrpc/android/XMLRPCSerializable.java @@ -0,0 +1,17 @@ +package org.xmlrpc.android; + +/** + * Allows to pass any XMLRPCSerializable object as input parameter. + * When implementing getSerializable() you should return + * one of XMLRPC primitive types (or another XMLRPCSerializable: be careful not going into + * recursion by passing this object reference!) + */ +public interface XMLRPCSerializable { + + /** + * Gets XMLRPC serialization object + * @return object to serialize This object is most likely one of XMLRPC primitive types, + * however you can return also another XMLRPCSerializable + */ + Object getSerializable(); +} diff --git a/src/org/xmlrpc/android/XMLRPCSerializer.java b/src/org/xmlrpc/android/XMLRPCSerializer.java @@ -0,0 +1,226 @@ +package org.xmlrpc.android; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +//import android.util.Log; + +class XMLRPCSerializer implements IXMLRPCSerializer { + static SimpleDateFormat dateFormat = new SimpleDateFormat(DATETIME_FORMAT); + + @SuppressWarnings("unchecked") + public void serialize(XmlSerializer serializer, Object object) throws IOException { + // This code supplied by mattias.ellback as part of issue #19 + if (object == null){ + serializer.startTag(null, TYPE_NULL).endTag(null, TYPE_NULL); + } else + // check for scalar types: + if (object instanceof Integer || object instanceof Short || object instanceof Byte) { + serializer.startTag(null, TYPE_I4).text(object.toString()).endTag(null, TYPE_I4); + } else + if (object instanceof Long) { + serializer.startTag(null, TYPE_I8).text(object.toString()).endTag(null, TYPE_I8); + } else + if (object instanceof Double || object instanceof Float) { + serializer.startTag(null, TYPE_DOUBLE).text(object.toString()).endTag(null, TYPE_DOUBLE); + } else + if (object instanceof Boolean) { + Boolean bool = (Boolean) object; + String boolStr = bool.booleanValue() ? "1" : "0"; + serializer.startTag(null, TYPE_BOOLEAN).text(boolStr).endTag(null, TYPE_BOOLEAN); + } else + if (object instanceof String) { + serializer.startTag(null, TYPE_STRING).text(object.toString()).endTag(null, TYPE_STRING); + } else + if (object instanceof Date || object instanceof Calendar) { + String dateStr = dateFormat.format(object); + serializer.startTag(null, TYPE_DATE_TIME_ISO8601).text(dateStr).endTag(null, TYPE_DATE_TIME_ISO8601); + } else + if (object instanceof byte[] ){ + String value = new String(Base64Coder.encode((byte[])object)); + serializer.startTag(null, TYPE_BASE64).text(value).endTag(null, TYPE_BASE64); + } else + if (object instanceof List) { + serializer.startTag(null, TYPE_ARRAY).startTag(null, TAG_DATA); + List<Object> list = (List<Object>) object; + Iterator<Object> iter = list.iterator(); + while (iter.hasNext()) { + Object o = iter.next(); + serializer.startTag(null, TAG_VALUE); + serialize(serializer, o); + serializer.endTag(null, TAG_VALUE); + } + serializer.endTag(null, TAG_DATA).endTag(null, TYPE_ARRAY); + } else + if (object instanceof Object[]) { + serializer.startTag(null, TYPE_ARRAY).startTag(null, TAG_DATA); + Object[] objects = (Object[]) object; + for (int i=0; i<objects.length; i++) { + Object o = objects[i]; + serializer.startTag(null, TAG_VALUE); + serialize(serializer, o); + serializer.endTag(null, TAG_VALUE); + } + serializer.endTag(null, TAG_DATA).endTag(null, TYPE_ARRAY); + } else + if (object instanceof Map) { + serializer.startTag(null, TYPE_STRUCT); + Map<String, Object> map = (Map<String, Object>) object; + Iterator<Entry<String, Object>> iter = map.entrySet().iterator(); + while (iter.hasNext()) { + Entry<String, Object> entry = iter.next(); + String key = entry.getKey(); + Object value = entry.getValue(); + + serializer.startTag(null, TAG_MEMBER); + serializer.startTag(null, TAG_NAME).text(key).endTag(null, TAG_NAME); + serializer.startTag(null, TAG_VALUE); + serialize(serializer, value); + serializer.endTag(null, TAG_VALUE); + serializer.endTag(null, TAG_MEMBER); + } + serializer.endTag(null, TYPE_STRUCT); + } else + if (object instanceof XMLRPCSerializable) { + XMLRPCSerializable serializable = (XMLRPCSerializable) object; + serialize(serializer, serializable.getSerializable()); + } else { + throw new IOException("Cannot serialize " + object); + } + } + + public Object deserialize(XmlPullParser parser) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, null, TAG_VALUE); + + if (parser.isEmptyElementTag()) { + // degenerated <value />, return empty string + return ""; + } + + Object obj; + boolean hasType = true; + String typeNodeName = null; + try { + parser.nextTag(); + typeNodeName = parser.getName(); + if (typeNodeName.equals(TAG_VALUE) && parser.getEventType() == XmlPullParser.END_TAG) { + // empty <value></value>, return empty string + return ""; + } + + + } catch (XmlPullParserException e) { + hasType = false; + } + if (hasType) { + // This code submitted by mattias.ellback in issue #19 + if (typeNodeName.equals(TYPE_NULL)){ + parser.nextTag(); + obj = null; + } + else + if (typeNodeName.equals(TYPE_INT) || typeNodeName.equals(TYPE_I4)) { + String value = parser.nextText(); + obj = Integer.parseInt(value); + } else + if (typeNodeName.equals(TYPE_I8)) { + String value = parser.nextText(); + obj = Long.parseLong(value); + } else + if (typeNodeName.equals(TYPE_DOUBLE)) { + String value = parser.nextText(); + obj = Double.parseDouble(value); + } else + if (typeNodeName.equals(TYPE_BOOLEAN)) { + String value = parser.nextText(); + obj = value.equals("1") ? Boolean.TRUE : Boolean.FALSE; + } else + if (typeNodeName.equals(TYPE_STRING)) { + obj = parser.nextText(); + } else + if (typeNodeName.equals(TYPE_DATE_TIME_ISO8601)) { + String value = parser.nextText(); + try { + obj = dateFormat.parseObject(value); + } catch (ParseException e) { + throw new IOException("Cannot deserialize dateTime " + value); + } + } else + if (typeNodeName.equals(TYPE_BASE64)) { + String value = parser.nextText(); + BufferedReader reader = new BufferedReader(new StringReader(value)); + String line; + StringBuffer sb = new StringBuffer(); + while ((line = reader.readLine()) != null) { + sb.append(line); + } + obj = Base64Coder.decode(sb.toString()); + } else + if (typeNodeName.equals(TYPE_ARRAY)) { + parser.nextTag(); // TAG_DATA (<data>) + parser.require(XmlPullParser.START_TAG, null, TAG_DATA); + + parser.nextTag(); + List<Object> list = new ArrayList<Object>(); + while (parser.getName().equals(TAG_VALUE)) { + list.add(deserialize(parser)); + parser.nextTag(); + } + parser.require(XmlPullParser.END_TAG, null, TAG_DATA); + parser.nextTag(); // TAG_ARRAY (</array>) + parser.require(XmlPullParser.END_TAG, null, TYPE_ARRAY); + obj = list.toArray(); + } else + if (typeNodeName.equals(TYPE_STRUCT)) { + parser.nextTag(); + Map<String, Object> map = new HashMap<String, Object>(); + while (parser.getName().equals(TAG_MEMBER)) { + String memberName = null; + Object memberValue = null; + while (true) { + parser.nextTag(); + String name = parser.getName(); + if (name.equals(TAG_NAME)) { + memberName = parser.nextText(); + } else + if (name.equals(TAG_VALUE)) { + memberValue = deserialize(parser); + } else { + break; + } + } + if (memberName != null && memberValue != null) { + map.put(memberName, memberValue); + } + parser.require(XmlPullParser.END_TAG, null, TAG_MEMBER); + parser.nextTag(); + } + parser.require(XmlPullParser.END_TAG, null, TYPE_STRUCT); + obj = map; + } else { + throw new IOException("Cannot deserialize " + parser.getName()); + } + } else { + // TYPE_STRING (<string>) is not required + obj = parser.getText(); + } + parser.nextTag(); // TAG_VALUE (</value>) + parser.require(XmlPullParser.END_TAG, null, TAG_VALUE); + return obj; + } +} diff --git a/src/org/xmlrpc/android/XMLRPCServer.java b/src/org/xmlrpc/android/XMLRPCServer.java @@ -0,0 +1,104 @@ +package org.xmlrpc.android; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.net.Socket; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.util.Log; + +public class XMLRPCServer extends XMLRPCCommon { + + private static final String RESPONSE = + "HTTP/1.1 200 OK\n" + + "Connection: close\n" + + "Content-Type: text/xml\n" + + "Content-Length: "; + private static final String NEWLINES = "\n\n"; + private XMLRPCSerializer iXMLRPCSerializer; + + public XMLRPCServer() { + iXMLRPCSerializer = new XMLRPCSerializer(); + } + + public MethodCall readMethodCall(Socket socket) throws IOException, XmlPullParserException + { + MethodCall methodCall = new MethodCall(); + InputStream inputStream = socket.getInputStream(); + + XmlPullParser pullParser = xmlPullParserFromSocket(inputStream); + + pullParser.nextTag(); + pullParser.require(XmlPullParser.START_TAG, null, Tag.METHOD_CALL); + pullParser.nextTag(); + pullParser.require(XmlPullParser.START_TAG, null, Tag.METHOD_NAME); + + methodCall.setMethodName(pullParser.nextText()); + + pullParser.nextTag(); + pullParser.require(XmlPullParser.START_TAG, null, Tag.PARAMS); + pullParser.nextTag(); // <param> + + do { + //Log.d(Tag.LOG, "type=" + pullParser.getEventType() + ", tag=" + pullParser.getName()); + pullParser.require(XmlPullParser.START_TAG, null, Tag.PARAM); + pullParser.nextTag(); // <value> + + Object param = iXMLRPCSerializer.deserialize(pullParser); + methodCall.params.add(param); // add to return value + + pullParser.nextTag(); + pullParser.require(XmlPullParser.END_TAG, null, Tag.PARAM); + pullParser.nextTag(); // <param> or </params> + + } while (!pullParser.getName().equals(Tag.PARAMS)); // </params> + + return methodCall; + } + + XmlPullParser xmlPullParserFromSocket(InputStream socketInputStream) throws IOException, XmlPullParserException { + String line; + BufferedReader br = new BufferedReader(new InputStreamReader(socketInputStream)); + while ((line = br.readLine()) != null && line.length() > 0); // eat the HTTP POST headers + + XmlPullParser pullParser = XmlPullParserFactory.newInstance().newPullParser(); + pullParser.setInput(br); + return pullParser; + } + + public void respond(Socket socket, Object[] params) throws IOException { + + String content = methodResponse(params); + String response = RESPONSE + (content.length()) + NEWLINES + content; + OutputStream outputStream = socket.getOutputStream(); + outputStream.write(response.getBytes()); + outputStream.flush(); + outputStream.close(); + socket.close(); + Log.d(Tag.LOG, "response:" + response); + } + + private String methodResponse(Object[] params) + throws IllegalArgumentException, IllegalStateException, IOException { + StringWriter bodyWriter = new StringWriter(); + serializer.setOutput(bodyWriter); + serializer.startDocument(null, null); + serializer.startTag(null, Tag.METHOD_RESPONSE); + + serializeParams(params); + + serializer.endTag(null, Tag.METHOD_RESPONSE); + serializer.endDocument(); + + return bodyWriter.toString(); + } +}+ \ No newline at end of file