/*
* Copyright 2010 Kevin Gaudin
*
* 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.acra.sender;
import static org.acra.ACRA.LOG_TAG;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import org.acra.ACRA;
import org.acra.ACRAConfiguration;
import org.acra.ACRAConstants;
import org.acra.ReportField;
import org.acra.annotation.ReportsCrashes;
import org.acra.collector.CrashReportData;
import org.acra.util.HttpRequest;
import org.acra.util.JSONReportBuilder.JSONReportException;
import android.net.Uri;
/**
* <p>
* The {@link ReportSender} used by ACRA when {@link ReportsCrashes#formUri()}
* has been defined in order to post crash data to a custom server-side data
* collection script. It sends all data in a POST request with parameters named
* with easy to understand names (basically a string conversion of
* {@link ReportField} enum values) or based on your own conversion Map from
* {@link ReportField} values to String.
* </p>
*
* <p>
* To use specific POST parameter names, you can provide your own report fields
* mapping scheme:
* </p>
*
* <pre>
* @ReportsCrashes(...)
* public class myApplication extends Application {
*
* public void onCreate() {
* super.onCreate();
* ACRA.init(this);
* Map<ReportField, String> mapping = new HashMap<ReportField, String>();
* mapping.put(ReportField.APP_VERSION_CODE, "myAppVerCode');
* mapping.put(ReportField.APP_VERSION_NAME, "myAppVerName');
* //...
* mapping.put(ReportField.USER_EMAIL, "userEmail');
* // remove any default report sender
* ErrorReporter.getInstance().removeAllReportSenders();
* // create your own instance with your specific mapping
* ErrorReporter.getInstance().addReportSender(new ReportSender("http://my.domain.com/reports/receiver.py", mapping));
* }
* }
* </pre>
*
*/
public class HttpSender implements ReportSender {
/**
* Available HTTP methods to send data. Only POST and PUT are currently
* supported.
*/
public enum Method {
POST, PUT
}
/**
* Type of report data encoding, currently supports Html Form encoding and
* JSON.
*/
public enum Type {
/**
* Send data as a www form encoded list of key/values.
* @see <a href="http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4">Form content types</a>
*/
FORM {
@Override
public String getContentType() {
return "application/x-www-form-urlencoded";
}
},
/**
* Send data as a structured JSON tree.
*/
JSON {
@Override
public String getContentType() {
return "application/json";
}
};
public abstract String getContentType();
}
private final Uri mFormUri;
private final Map<ReportField, String> mMapping;
private final Method mMethod;
private final Type mType;
private String mUsername;
private String mPassword;
/**
* <p>
* Create a new HttpSender instance with its destination taken from
* {@link ACRA#getConfig()} dynamically. Configuration changes to the
* formUri are applied automatically.
* </p>
*
* @param method
* HTTP {@link Method} to be used to send data. Currently only
* {@link Method#POST} and {@link Method#PUT} are available. If
* {@link Method#PUT} is used, the {@link ReportField#REPORT_ID}
* is appended to the formUri to be compliant with RESTful APIs.
*
* @param type
* {@link Type} of encoding used to send the report body.
* {@link Type#FORM} is a simple Key/Value pairs list as defined
* by the application/x-www-form-urlencoded mime type.
*
* @param mapping
* Applies only to {@link Method#POST} method parameter. If null,
* POST parameters will be named with {@link ReportField} values
* converted to String with .toString(). If not null, POST
* parameters will be named with the result of
* mapping.get(ReportField.SOME_FIELD);
*/
public HttpSender(Method method, Type type, Map<ReportField, String> mapping) {
mMethod = method;
mFormUri = null;
mMapping = mapping;
mType = type;
mUsername = null;
mPassword = null;
}
/**
* <p>
* Create a new HttpPostSender instance with a fixed destination provided as
* a parameter. Configuration changes to the formUri are not applied.
* </p>
*
* @param method
* HTTP {@link Method} to be used to send data. Currently only
* {@link Method#POST} and {@link Method#PUT} are available. If
* {@link Method#PUT} is used, the {@link ReportField#REPORT_ID}
* is appended to the formUri to be compliant with RESTful APIs.
*
* @param type
* {@link Type} of encoding used to send the report body.
* {@link Type#FORM} is a simple Key/Value pairs list as defined
* by the application/x-www-form-urlencoded mime type.
* @param formUri
* The URL of your server-side crash report collection script.
* @param mapping
* Applies only to {@link Method#POST} method parameter. If null,
* POST parameters will be named with {@link ReportField} values
* converted to String with .toString(). If not null, POST
* parameters will be named with the result of
* mapping.get(ReportField.SOME_FIELD);
*/
public HttpSender(Method method, Type type, String formUri, Map<ReportField, String> mapping) {
mMethod = method;
mFormUri = Uri.parse(formUri);
mMapping = mapping;
mType = type;
mUsername = null;
mPassword = null;
}
/**
* <p>
* Set credentials for this HttpSender that override (if present) the ones
* set globally.
* </p>
*
* @param username
* The username to set for HTTP Basic Auth.
* @param password
* The password to set for HTTP Basic Auth.
*/
@SuppressWarnings( "unused" )
public void setBasicAuth(String username, String password) {
mUsername = username;
mPassword = password;
}
@Override
public void send(Context context, CrashReportData report) throws ReportSenderException {
try {
URL reportUrl = mFormUri == null ? new URL(ACRA.getConfig().formUri()) : new URL(mFormUri.toString());
ACRA.log.d(LOG_TAG, "Connect to " + reportUrl.toString());
final String login = mUsername != null ? mUsername : ACRAConfiguration.isNull(ACRA.getConfig().formUriBasicAuthLogin()) ? null : ACRA
.getConfig().formUriBasicAuthLogin();
final String password = mPassword != null ? mPassword : ACRAConfiguration.isNull(ACRA.getConfig().formUriBasicAuthPassword()) ? null : ACRA
.getConfig().formUriBasicAuthPassword();
final HttpRequest request = new HttpRequest();
request.setConnectionTimeOut(ACRA.getConfig().connectionTimeout());
request.setSocketTimeOut(ACRA.getConfig().socketTimeout());
request.setMaxNrRetries(ACRA.getConfig().maxNumberOfRequestRetries());
request.setLogin(login);
request.setPassword(password);
request.setHeaders(ACRA.getConfig().getHttpHeaders());
// Generate report body depending on requested type
final String reportAsString;
switch (mType) {
case JSON:
reportAsString = report.toJSON().toString();
break;
case FORM:
default:
final Map<String, String> finalReport = remap(report);
reportAsString = HttpRequest.getParamsAsFormString(finalReport);
break;
}
// Adjust URL depending on method
switch (mMethod) {
case POST:
break;
case PUT:
reportUrl = new URL(reportUrl.toString() + '/' + report.getProperty(ReportField.REPORT_ID));
break;
default:
throw new UnsupportedOperationException("Unknown method: " + mMethod.name());
}
request.send(context, reportUrl, mMethod, reportAsString, mType);
} catch (IOException e) {
throw new ReportSenderException("Error while sending " + ACRA.getConfig().reportType()
+ " report via Http " + mMethod.name(), e);
} catch (JSONReportException e) {
throw new ReportSenderException("Error while sending " + ACRA.getConfig().reportType()
+ " report via Http " + mMethod.name(), e);
}
}
private Map<String, String> remap(Map<ReportField, String> report) {
ReportField[] fields = ACRA.getConfig().customReportContent();
if (fields.length == 0) {
fields = ACRAConstants.DEFAULT_REPORT_FIELDS;
}
final Map<String, String> finalReport = new HashMap<String, String>(report.size());
for (ReportField field : fields) {
if (mMapping == null || mMapping.get(field) == null) {
finalReport.put(field.toString(), report.get(field));
} else {
finalReport.put(mMapping.get(field), report.get(field));
}
}
return finalReport;
}
}