summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Dolgov <[email protected]>2011-11-29 07:03:38 +0300
committerAndrew Dolgov <[email protected]>2011-11-29 07:03:38 +0300
commit9a4c7a2ce8b0d31496291e6f3fede26a8c8f3d30 (patch)
tree3f0034db14e3547bec7a1c0906f78d36526a8e19
parent9d0a0426fdf91366dd0beb5418b8093ffab98a4b (diff)
improve error handling by ApiRequest
-rw-r--r--res/values/strings.xml27
-rw-r--r--src/org/fox/ttrss/ApiRequest.java133
-rw-r--r--src/org/fox/ttrss/ArticleFragment.java40
-rw-r--r--src/org/fox/ttrss/FeedCategoriesFragment.java68
-rw-r--r--src/org/fox/ttrss/FeedsFragment.java70
-rw-r--r--src/org/fox/ttrss/HeadlinesFragment.java81
-rw-r--r--src/org/fox/ttrss/MainActivity.java71
7 files changed, 275 insertions, 215 deletions
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9f82a190..2bd63ab7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -23,9 +23,6 @@
<string name="theme_light">Light</string>
<string name="connection">Connection</string>
<string name="http_authentication">HTTP Authentication</string>
- <string name="login_api_disabled">Login failed: API disabled.</string>
- <string name="login_no_data">Login failed: no data received.</string>
- <string name="login_wrong_password">Login failed: username or password incorrect.</string>
<string name="login_success">Logged in.</string>
<string name="no_unread_feeds">No unread feeds.</string>
<string name="no_unread_headlines">No unread headlines.</string>
@@ -35,15 +32,11 @@
<string name="update_feeds">Refresh feeds</string>
<string name="close_article">Close article</string>
<string name="share_article">Share article</string>
- <string name="could_not_decode_content">Could not decode content (UnsupportedEncodingException)</string>
<string name="sort_feeds_by_unread">Sort feeds by unread count</string>
<string name="load_more_articles">Load more...</string>
<string name="show_all_articles">Show all articles</string>
<string name="show_unread_articles">Show unread articles</string>
<string name="ssl_trust_any">Accept any SSL certificate</string>
- <string name="error_no_data">Error: no data received.</string>
- <string name="error_no_feeds">No feeds to display.</string>
- <string name="error_invalid_object">Error: invalid object received.</string>
<string name="blank"></string>
<string name="transport_debugging">Log sent and received data</string>
<string name="toggle_marked">Toggle starred</string>
@@ -57,4 +50,24 @@
<string name="download_feed_icons">Download and display feed icons</string>
<string name="enable_cats">Enable feed categories</string>
<string name="back_to_categories">Close category</string>
+ <string name="no_feeds_to_display">No feeds to display</string>
+ <string name="no_headlines_to_display">No headlines to display</string>
+
+ <string name="error_template">Error: %1$s</string>
+
+ <string name="error_no_error">No error</string>
+ <string name="error_unknown">Error: Unknown error (see log)</string>
+ <string name="error_http_unauthorized">Error: 401 unauthorized</string>
+ <string name="error_http_forbidden">Error: 403 forbidden</string>
+ <string name="error_http_not_found">Error: 404 not found</string>
+ <string name="error_http_server_error">Error: 500 server error</string>
+ <string name="error_http_other_error">Error: other HTTP error (see log)</string>
+ <string name="error_ssl_rejected">Error: SSL certificate rejected</string>
+ <string name="error_parse_error">Error: JSON parse failed</string>
+ <string name="error_io_error">Error: I/O failure (server down?)</string>
+ <string name="error_other_error">Error: unknown error (see log)</string>
+ <string name="error_api_disabled">Error: API disabled for this user</string>
+ <string name="error_api_unknown">Error: unknown API error (see log)</string>
+ <string name="error_login_failed">Error: username or password incorrect</string>
+
</resources>
diff --git a/src/org/fox/ttrss/ApiRequest.java b/src/org/fox/ttrss/ApiRequest.java
index 9af9d33f..1611cbb9 100644
--- a/src/org/fox/ttrss/ApiRequest.java
+++ b/src/org/fox/ttrss/ApiRequest.java
@@ -1,6 +1,7 @@
package org.fox.ttrss;
import java.io.BufferedReader;
+import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
@@ -29,21 +30,27 @@ import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonElement> {
private final String TAG = this.getClass().getSimpleName();
- protected static final int STATUS_LOGIN_FAILED = 0;
- protected static final int STATUS_OK = 1;
- protected static final int STATUS_API_DISABLED = 2;
- protected static final int STATUS_OTHER_ERROR = 3;
+ public enum ApiError { NO_ERROR, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND,
+ HTTP_SERVER_ERROR, HTTP_OTHER_ERROR, SSL_REJECTED, PARSE_ERROR, IO_ERROR, OTHER_ERROR, API_DISABLED, API_UNKNOWN, LOGIN_FAILED };
+ public static final int API_STATUS_OK = 0;
+ public static final int API_STATUS_ERR = 1;
+
private String m_api;
private boolean m_trustAny = false;
private boolean m_transportDebugging = false;
+ protected int m_httpStatusCode = 0;
+ protected int m_apiStatusCode = 0;
private Context m_context;
private SharedPreferences m_prefs;
+
+ protected ApiError m_lastError;
public ApiRequest(Context context) {
m_context = context;
@@ -53,6 +60,42 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
m_api = m_prefs.getString("ttrss_url", null);
m_trustAny = m_prefs.getBoolean("ssl_trust_any", false);
m_transportDebugging = m_prefs.getBoolean("transport_debugging", false);
+ m_lastError = ApiError.NO_ERROR;
+
+ }
+
+ protected int getErrorMessage() {
+ switch (m_lastError) {
+ case NO_ERROR:
+ return R.string.error_unknown;
+ case HTTP_UNAUTHORIZED:
+ return R.string.error_http_unauthorized;
+ case HTTP_FORBIDDEN:
+ return R.string.error_http_forbidden;
+ case HTTP_NOT_FOUND:
+ return R.string.error_http_not_found;
+ case HTTP_SERVER_ERROR:
+ return R.string.error_http_server_error;
+ case HTTP_OTHER_ERROR:
+ return R.string.error_http_other_error;
+ case SSL_REJECTED:
+ return R.string.error_ssl_rejected;
+ case PARSE_ERROR:
+ return R.string.error_parse_error;
+ case IO_ERROR:
+ return R.string.error_io_error;
+ case OTHER_ERROR:
+ return R.string.error_other_error;
+ case API_DISABLED:
+ return R.string.error_api_disabled;
+ case API_UNKNOWN:
+ return R.string.error_api_unknown;
+ case LOGIN_FAILED:
+ return R.string.error_login_failed;
+ default:
+ Log.d(TAG, "getErrorMessage: unknown error code=" + m_lastError);
+ return R.string.error_unknown;
+ }
}
@Override
@@ -106,25 +149,77 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
httpPost.setEntity(new StringEntity(requestStr, "utf-8"));
HttpResponse execute = client.execute(httpPost);
- InputStream content = execute.getEntity().getContent();
-
- BufferedReader buffer = new BufferedReader(
- new InputStreamReader(content), 8192);
-
- String s = "";
- String response = "";
+ m_httpStatusCode = execute.getStatusLine().getStatusCode();
- while ((s = buffer.readLine()) != null) {
- response += s;
+ switch (m_httpStatusCode) {
+ case 200:
+ InputStream content = execute.getEntity().getContent();
+
+ BufferedReader buffer = new BufferedReader(
+ new InputStreamReader(content), 8192);
+
+ String s = "";
+ String response = "";
+
+ while ((s = buffer.readLine()) != null) {
+ response += s;
+ }
+
+ if (m_transportDebugging) Log.d(TAG, "<<< " + response);
+
+ JsonParser parser = new JsonParser();
+
+ JsonElement result = parser.parse(response);
+ JsonObject resultObj = result.getAsJsonObject();
+
+ m_apiStatusCode = resultObj.get("status").getAsInt();
+
+ switch (m_apiStatusCode) {
+ case API_STATUS_OK:
+ return result.getAsJsonObject().get("content");
+ case API_STATUS_ERR:
+ JsonObject contentObj = resultObj.get("content").getAsJsonObject();
+ String error = contentObj.get("error").getAsString();
+
+ if (error.equals("LOGIN_ERROR")) {
+ m_lastError = ApiError.LOGIN_FAILED;
+ } else if (error.equals("API_DISABLED")) {
+ m_lastError = ApiError.LOGIN_FAILED;
+ } else {
+ m_lastError = ApiError.API_UNKNOWN;
+ }
+ }
+
+ return null;
+ case 401:
+ m_lastError = ApiError.HTTP_UNAUTHORIZED;
+ break;
+ case 403:
+ m_lastError = ApiError.HTTP_FORBIDDEN;
+ break;
+ case 404:
+ m_lastError = ApiError.HTTP_NOT_FOUND;
+ break;
+ case 500:
+ m_lastError = ApiError.HTTP_SERVER_ERROR;
+ break;
+ default:
+ m_lastError = ApiError.HTTP_OTHER_ERROR;
+ break;
}
-
- if (m_transportDebugging) Log.d(TAG, "<<< " + response);
-
- JsonParser parser = new JsonParser();
-
- return parser.parse(response);
+ return null;
+ } catch (javax.net.ssl.SSLPeerUnverifiedException e) {
+ m_lastError = ApiError.SSL_REJECTED;
+ e.printStackTrace();
+ } catch (IOException e) {
+ m_lastError = ApiError.IO_ERROR;
+ e.printStackTrace();
+ } catch (com.google.gson.JsonSyntaxException e) {
+ m_lastError = ApiError.PARSE_ERROR;
+ e.printStackTrace();
} catch (Exception e) {
+ m_lastError = ApiError.OTHER_ERROR;
e.printStackTrace();
}
diff --git a/src/org/fox/ttrss/ArticleFragment.java b/src/org/fox/ttrss/ArticleFragment.java
index 9fc2619b..a2146838 100644
--- a/src/org/fox/ttrss/ArticleFragment.java
+++ b/src/org/fox/ttrss/ArticleFragment.java
@@ -94,31 +94,25 @@ public class ArticleFragment extends Fragment implements OnClickListener {
// this is ridiculous
// TODO white on black style for dark theme
String content;
- try {
- String cssOverride = "";
-
- if (m_prefs.getString("theme", "THEME_DARK").equals("THEME_DARK")) {
- web.setBackgroundColor(android.R.color.black);
- cssOverride = "body { background : black; color : #f0f0f0}\n";
- }
-
- content =
- "<html>" +
- "<head>" +
- "<meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\">" +
- "<style type=\"text/css\">" +
- cssOverride +
- "img { max-width : 90%; }" +
- "body { text-align : justify; }" +
- "</style>" +
- "</head>" +
- "<body>" + m_article.content + "</body></html>";
-
- } catch (Exception e) {
- content = getString(R.string.could_not_decode_content);
- e.printStackTrace();
+ String cssOverride = "";
+
+ if (m_prefs.getString("theme", "THEME_DARK").equals("THEME_DARK")) {
+ web.setBackgroundColor(android.R.color.black);
+ cssOverride = "body { background : black; color : #f0f0f0}\n";
}
+ content =
+ "<html>" +
+ "<head>" +
+ "<meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\">" +
+ "<style type=\"text/css\">" +
+ cssOverride +
+ "img { max-width : 90%; }" +
+ "body { text-align : justify; }" +
+ "</style>" +
+ "</head>" +
+ "<body>" + m_article.content + "</body></html>";
+
web.loadDataWithBaseURL(null, content, "text/html", "utf-8", null);
if (activity.isSmallScreen())
diff --git a/src/org/fox/ttrss/FeedCategoriesFragment.java b/src/org/fox/ttrss/FeedCategoriesFragment.java
index 8c1cd9b5..5ef5b504 100644
--- a/src/org/fox/ttrss/FeedCategoriesFragment.java
+++ b/src/org/fox/ttrss/FeedCategoriesFragment.java
@@ -14,6 +14,7 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -162,52 +163,43 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
public CatsRequest(Context context) {
super(context);
}
-
+
protected void onPostExecute(JsonElement result) {
if (result != null) {
try {
- JsonObject rv = result.getAsJsonObject();
-
- Gson gson = new Gson();
-
- int status = rv.get("status").getAsInt();
-
- if (status == 0) {
- JsonArray content = rv.get("content").getAsJsonArray();
- if (content != null) {
- Type listType = new TypeToken<List<FeedCategory>>() {}.getType();
- final List<FeedCategory> cats = gson.fromJson(content, listType);
-
- m_cats.clear();
-
- for (FeedCategory c : cats)
- m_cats.add(c);
-
- sortCats();
-
- if (m_cats.size() == 0)
- setLoadingStatus(R.string.error_no_feeds, false);
- else
- setLoadingStatus(R.string.blank, false);
-
- }
- } else {
- MainActivity activity = (MainActivity)getActivity();
- activity.login();
+ JsonArray content = result.getAsJsonArray();
+ if (content != null) {
+ Type listType = new TypeToken<List<FeedCategory>>() {}.getType();
+ final List<FeedCategory> cats = new Gson().fromJson(content, listType);
+
+ m_cats.clear();
+
+ for (FeedCategory c : cats)
+ m_cats.add(c);
+
+ sortCats();
+
+ if (m_cats.size() == 0)
+ setLoadingStatus(R.string.no_feeds_to_display, false);
+ else
+ setLoadingStatus(R.string.blank, false);
+
+ return;
}
+
} catch (Exception e) {
- e.printStackTrace();
- setLoadingStatus(R.string.error_invalid_object, false);
- // report invalid object received
+ e.printStackTrace();
}
+ }
+
+ if (m_lastError == ApiError.LOGIN_FAILED) {
+ MainActivity activity = (MainActivity)getActivity();
+ activity.login();
} else {
- // report null object received, unless we've been awakened from sleep right in the right time
- // so that current request failed
- if (m_cats.size() == 0) setLoadingStatus(R.string.error_no_data, false);
+ setLoadingStatus(getErrorMessage(), false);
}
-
- return;
- }
+ }
+
}
public void sortCats() {
diff --git a/src/org/fox/ttrss/FeedsFragment.java b/src/org/fox/ttrss/FeedsFragment.java
index a2438b27..6243f79b 100644
--- a/src/org/fox/ttrss/FeedsFragment.java
+++ b/src/org/fox/ttrss/FeedsFragment.java
@@ -51,7 +51,6 @@ import android.widget.TextView;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
public class FeedsFragment extends Fragment implements OnItemClickListener, OnSharedPreferenceChangeListener {
@@ -220,7 +219,7 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
if (result != null) {
try {
- JsonElement iconsUrl = result.getAsJsonObject().get("content").getAsJsonObject().get("icons_dir");
+ JsonElement iconsUrl = result.getAsJsonObject().get("icons_dir");
if (iconsUrl != null) {
String iconsStr = iconsUrl.getAsString();
@@ -261,54 +260,45 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
public FeedsRequest(Context context) {
super(context);
}
-
+
protected void onPostExecute(JsonElement result) {
if (result != null) {
try {
- JsonObject rv = result.getAsJsonObject();
+ JsonArray content = result.getAsJsonArray();
+ if (content != null) {
- Gson gson = new Gson();
-
- int status = rv.get("status").getAsInt();
-
- if (status == 0) {
- JsonArray content = rv.get("content").getAsJsonArray();
- if (content != null) {
- Type listType = new TypeToken<List<Feed>>() {}.getType();
- final List<Feed> feeds = gson.fromJson(content, listType);
-
- m_feeds.clear();
-
- for (Feed f : feeds)
- if (f.id > -10) // skip labels for now
- m_feeds.add(f);
-
- sortFeeds();
-
- if (m_feeds.size() == 0)
- setLoadingStatus(R.string.error_no_feeds, false);
- else
- setLoadingStatus(R.string.blank, false);
+ Type listType = new TypeToken<List<Feed>>() {}.getType();
+ final List<Feed> feeds = new Gson().fromJson(content, listType);
+
+ m_feeds.clear();
+
+ for (Feed f : feeds)
+ if (f.id > -10) // skip labels for now
+ m_feeds.add(f);
+
+ sortFeeds();
+
+ if (m_feeds.size() == 0)
+ setLoadingStatus(R.string.no_feeds_to_display, false);
+ else
+ setLoadingStatus(R.string.blank, false);
- if (m_enableFeedIcons) getFeedIcons();
-
- }
- } else {
- MainActivity activity = (MainActivity)getActivity();
- activity.login();
+ if (m_enableFeedIcons) getFeedIcons();
+
+ return;
}
+
} catch (Exception e) {
- e.printStackTrace();
- setLoadingStatus(R.string.error_invalid_object, false);
- // report invalid object received
+ e.printStackTrace();
}
+ }
+
+ if (m_lastError == ApiError.LOGIN_FAILED) {
+ MainActivity activity = (MainActivity)getActivity();
+ activity.login();
} else {
- // report null object received, unless we've been awakened from sleep right in the right time
- // so that current request failed
- if (m_feeds.size() == 0) setLoadingStatus(R.string.error_no_data, false);
+ setLoadingStatus(getErrorMessage(), false);
}
-
- return;
}
}
diff --git a/src/org/fox/ttrss/HeadlinesFragment.java b/src/org/fox/ttrss/HeadlinesFragment.java
index 8e0d9756..defd5ee0 100644
--- a/src/org/fox/ttrss/HeadlinesFragment.java
+++ b/src/org/fox/ttrss/HeadlinesFragment.java
@@ -34,7 +34,6 @@ import android.widget.TextView;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
public class HeadlinesFragment extends Fragment implements OnItemClickListener {
@@ -174,54 +173,50 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener {
protected void onPostExecute(JsonElement result) {
if (result != null) {
try {
- JsonObject rv = result.getAsJsonObject();
-
- Gson gson = new Gson();
-
- int status = rv.get("status").getAsInt();
-
- if (status == 0) {
- JsonArray content = rv.get("content").getAsJsonArray();
- if (content != null) {
- Type listType = new TypeToken<List<Article>>() {}.getType();
- final List<Article> articles = gson.fromJson(content, listType);
-
- if (m_offset == 0)
- m_articles.clear();
-
- int last_position = m_articles.size();
-
- for (Article f : articles)
- m_articles.add(f);
-
- m_adapter.notifyDataSetChanged();
-
- ListView list = (ListView)getView().findViewById(R.id.headlines);
-
- if (list != null && m_offset != 0) {
- list.setSelection(last_position+1);
- }
-
- MainActivity activity = (MainActivity)getActivity();
- activity.setCanLoadMore(articles.size() >= 30);
- activity.initMainMenu();
-
- setLoadingStatus(R.string.blank, false);
+ JsonArray content = result.getAsJsonArray();
+ if (content != null) {
+ Type listType = new TypeToken<List<Article>>() {}.getType();
+ final List<Article> articles = new Gson().fromJson(content, listType);
+
+ if (m_offset == 0)
+ m_articles.clear();
+
+ int last_position = m_articles.size();
+
+ for (Article f : articles)
+ m_articles.add(f);
+
+ m_adapter.notifyDataSetChanged();
+
+ ListView list = (ListView)getView().findViewById(R.id.headlines);
+
+ if (list != null && m_offset != 0) {
+ list.setSelection(last_position+1);
}
- } else {
- MainActivity activity = (MainActivity)getActivity();
- activity.login();
+
+ MainActivity activity = (MainActivity)getActivity();
+ activity.setCanLoadMore(articles.size() >= 30);
+ activity.initMainMenu();
+
+ if (m_articles.size() == 0)
+ setLoadingStatus(R.string.no_headlines_to_display, false);
+ else
+ setLoadingStatus(R.string.blank, false);
+
+ return;
}
+
} catch (Exception e) {
- e.printStackTrace();
- setLoadingStatus(R.string.error_invalid_object, false);
+ e.printStackTrace();
}
- } else {
- setLoadingStatus(R.string.error_no_data, false);
}
-
- return;
+ if (m_lastError == ApiError.LOGIN_FAILED) {
+ MainActivity activity = (MainActivity)getActivity();
+ activity.login();
+ } else {
+ setLoadingStatus(getErrorMessage(), false);
+ }
}
public void setOffset(int skip) {
diff --git a/src/org/fox/ttrss/MainActivity.java b/src/org/fox/ttrss/MainActivity.java
index af6e239a..a5ffc0ed 100644
--- a/src/org/fox/ttrss/MainActivity.java
+++ b/src/org/fox/ttrss/MainActivity.java
@@ -633,60 +633,41 @@ public class MainActivity extends FragmentActivity implements FeedsFragment.OnFe
protected void onPostExecute(JsonElement result) {
if (result != null) {
try {
- JsonObject rv = result.getAsJsonObject();
-
- int status = rv.get("status").getAsInt();
-
- if (status == 0) {
- JsonObject content = rv.get("content").getAsJsonObject();
- if (content != null) {
- m_sessionId = content.get("session_id").getAsString();
-
- Log.d(TAG, "Authenticated!");
-
- setLoadingStatus(R.string.loading_message, true);
+ JsonObject content = result.getAsJsonObject();
+ if (content != null) {
+ m_sessionId = content.get("session_id").getAsString();
+
+ Log.d(TAG, "Authenticated!");
+
+ setLoadingStatus(R.string.loading_message, true);
- FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
-
- if (m_enableCats) {
- FeedCategoriesFragment frag = new FeedCategoriesFragment();
- ft.replace(R.id.cats_fragment, frag);
- } else {
- FeedsFragment frag = new FeedsFragment();
- ft.replace(R.id.feeds_fragment, frag);
- }
-
- ft.commit();
-
- loginSuccess();
-
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+
+ if (m_enableCats) {
+ FeedCategoriesFragment frag = new FeedCategoriesFragment();
+ ft.replace(R.id.cats_fragment, frag);
+ } else {
+ FeedsFragment frag = new FeedsFragment();
+ ft.replace(R.id.feeds_fragment, frag);
}
- } else {
- JsonObject content = rv.get("content").getAsJsonObject();
+
+ ft.commit();
- if (content != null) {
- String error = content.get("error").getAsString();
-
- m_sessionId = null;
-
- if (error.equals("LOGIN_ERROR")) {
- setLoadingStatus(R.string.login_wrong_password, false);
- } else if (error.equals("API_DISABLED")) {
- setLoadingStatus(R.string.login_api_disabled, false);
- } else {
- setLoadingStatus(R.string.login_failed, false);
- }
-
- m_menu.findItem(R.id.login).setVisible(true);
- }
+ loginSuccess();
+ return;
}
+
} catch (Exception e) {
e.printStackTrace();
}
- } else {
- setLoadingStatus(R.string.login_no_data, false);
}
+
+ m_sessionId = null;
+
+ setLoadingStatus(getErrorMessage(), false);
+ m_menu.findItem(R.id.login).setVisible(true);
}
+
}
@Override