/*
* 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 android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Pair;
import org.acra.ACRA;
import org.acra.ACRAConstants;
import org.acra.ReportField;
import org.acra.annotation.ReportsCrashes;
import org.acra.attachment.DefaultAttachmentProvider;
import org.acra.collections.ImmutableSet;
import org.acra.collector.CrashReportData;
import org.acra.config.ACRAConfiguration;
import org.acra.http.BinaryHttpRequest;
import org.acra.http.DefaultHttpRequest;
import org.acra.http.HttpUtils;
import org.acra.http.MultipartHttpRequest;
import org.acra.model.Element;
import org.acra.util.InstanceCreator;
import org.json.JSONObject;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.acra.ACRA.LOG_TAG;
/**
* <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>
* Just create and declare a {@link ReportSenderFactory} that constructs a mapping
* from each {@link ReportField} to another name.
* </pre>
*/
public class HttpSender implements ReportSender {
/**
* Available HTTP methods to send data. Only POST and PUT are currently
* supported.
*/
public enum Method {
POST {
@Override
URL createURL(String baseUrl, CrashReportData report) throws MalformedURLException {
return new URL(baseUrl);
}
},
PUT {
@Override
URL createURL(String baseUrl, CrashReportData report) throws MalformedURLException {
return new URL(baseUrl + '/' + report.getProperty(ReportField.REPORT_ID));
}
};
abstract URL createURL(String baseUrl, CrashReportData report) throws MalformedURLException;
}
/**
* 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("application/x-www-form-urlencoded") {
@Override
String convertReport(HttpSender sender, CrashReportData report) throws IOException {
return HttpUtils.getParamsAsFormString(sender.convertToForm(report));
}
},
/**
* Send data as a structured JSON tree.
*/
JSON("application/json") {
@Override
String convertReport(HttpSender sender, CrashReportData report) throws IOException {
return sender.convertToJson(report).toString();
}
};
private final String contentType;
Type(String contentType) {
this.contentType = contentType;
}
@NonNull
public String getContentType() {
return contentType;
}
abstract String convertReport(HttpSender sender, CrashReportData report) throws IOException;
}
private final ACRAConfiguration config;
@Nullable
private final Uri mFormUri;
private final Map<ReportField, String> mMapping;
private final Method mMethod;
private final Type mType;
@Nullable
private String mUsername;
@Nullable
private String mPassword;
/**
* <p>
* Create a new HttpSender instance with its destination taken from the supplied config.
* Uses {@link ReportField} values converted to String with .toString() as form parameters.
* </p>
*
* @param config AcraConfig declaring the
* @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.
*/
public HttpSender(@NonNull ACRAConfiguration config, @NonNull Method method, @NonNull Type type) {
this(config, method, type, null);
}
/**
* <p>
* Create a new HttpSender instance with its destination taken from the supplied config.
* </p>
*
* @param config AcraConfig declaring the
* @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(@NonNull ACRAConfiguration config, @NonNull Method method, @NonNull Type type, @Nullable Map<ReportField, String> mapping) {
this(config, method, type, null, mapping);
}
/**
* <p>
* Create a new HttpPostSender instance with a fixed destination provided as
* a parameter. Configuration changes to the formUri are not applied.
* </p>
*
* @param config AcraConfig declaring the
* @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(@NonNull ACRAConfiguration config, @NonNull Method method, @NonNull Type type, @Nullable String formUri, @Nullable Map<ReportField, String> mapping) {
this.config = config;
mMethod = method;
mFormUri = (formUri == null) ? null : 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(@Nullable String username, @Nullable String password) {
mUsername = username;
mPassword = password;
}
@Override
public void send(@NonNull Context context, @NonNull CrashReportData report) throws ReportSenderException {
try {
final String baseUrl = mFormUri == null ? config.formUri() : mFormUri.toString();
if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Connect to " + baseUrl);
final String login = mUsername != null ? mUsername : isNull(config.formUriBasicAuthLogin()) ? null : config.formUriBasicAuthLogin();
final String password = mPassword != null ? mPassword : isNull(config.formUriBasicAuthPassword()) ? null : config.formUriBasicAuthPassword();
final InstanceCreator instanceCreator = new InstanceCreator();
final List<Uri> uris = instanceCreator.create(config.attachmentUriProvider(), new DefaultAttachmentProvider()).getAttachments(context, config);
// Generate report body depending on requested type
final String reportAsString = mType.convertReport(this, report);
// Adjust URL depending on method
final URL reportUrl = mMethod.createURL(baseUrl, report);
sendHttpRequests(config, context, mMethod, mType, login, password, config.connectionTimeout(),
config.socketTimeout(), config.httpHeaders(), reportAsString, reportUrl, uris);
} catch (@NonNull IOException e) {
throw new ReportSenderException("Error while sending " + config.reportType()
+ " report via Http " + mMethod.name(), e);
}
}
@SuppressWarnings("WeakerAccess")
protected void sendHttpRequests(@NonNull ACRAConfiguration configuration, @NonNull Context context, @NonNull Method method, @NonNull Type type,
@Nullable String login, @Nullable String password, int connectionTimeOut, int socketTimeOut, @Nullable Map<String, String> headers,
@NonNull String content, @NonNull URL url, @NonNull List<Uri> attachments) throws IOException {
switch (method) {
case POST:
if (attachments.isEmpty()) {
sendWithoutAttachments(configuration, context, method, type, login, password, connectionTimeOut, socketTimeOut, headers, content, url);
} else {
postMultipart(configuration, context, type, login, password, connectionTimeOut, socketTimeOut, headers, content, url, attachments);
}
break;
case PUT:
sendWithoutAttachments(configuration, context, method, type, login, password, connectionTimeOut, socketTimeOut, headers, content, url);
for (Uri uri : attachments) {
putAttachment(configuration, context, login, password, connectionTimeOut, socketTimeOut, headers, url, uri);
}
break;
}
}
@SuppressWarnings("WeakerAccess")
protected void sendWithoutAttachments(@NonNull ACRAConfiguration configuration, @NonNull Context context, @NonNull Method method, @NonNull Type type,
@Nullable String login, @Nullable String password, int connectionTimeOut, int socketTimeOut, @Nullable Map<String, String> headers,
@NonNull String content, @NonNull URL url) throws IOException {
new DefaultHttpRequest(configuration, context, method, type, login, password, connectionTimeOut, socketTimeOut, headers).send(url, content);
}
@SuppressWarnings("WeakerAccess")
protected void postMultipart(@NonNull ACRAConfiguration configuration, @NonNull Context context, @NonNull Type type,
@Nullable String login, @Nullable String password, int connectionTimeOut, int socketTimeOut, @Nullable Map<String, String> headers,
@NonNull String content, @NonNull URL url, @NonNull List<Uri> attachments) throws IOException {
new MultipartHttpRequest(configuration, context, type, login, password, connectionTimeOut, socketTimeOut, headers).send(url, Pair.create(content, attachments));
}
@SuppressWarnings("WeakerAccess")
protected void putAttachment(@NonNull ACRAConfiguration configuration, @NonNull Context context,
@Nullable String login, @Nullable String password, int connectionTimeOut, int socketTimeOut, @Nullable Map<String, String> headers,
@NonNull URL url, @NonNull Uri attachment) throws IOException {
final URL attachmentUrl = new URL(url.toString() + "-" + HttpUtils.getFileNameFromUri(context, attachment));
new BinaryHttpRequest(configuration, context, Method.PUT, login, password, connectionTimeOut, socketTimeOut, headers).send(attachmentUrl, attachment);
}
/**
* Convert a report to json
*
* @param report the report to convert
* @return a json representation of the report
*/
@SuppressWarnings("WeakerAccess")
protected JSONObject convertToJson(CrashReportData report) {
return report.toJSON();
}
/**
* Convert a report to a form-prepared map
*
* @param report the report to convert
* @return a form representation of the report
*/
@SuppressWarnings("WeakerAccess")
protected Map<String, String> convertToForm(CrashReportData report) {
return remap(report);
}
@NonNull
private Map<String, String> remap(@NonNull Map<ReportField, Element> report) {
Set<ReportField> fields = config.reportContent();
if (fields.isEmpty()) {
fields = new ImmutableSet<ReportField>(ACRAConstants.DEFAULT_REPORT_FIELDS);
}
final Map<String, String> finalReport = new HashMap<String, String>(report.size());
for (ReportField field : fields) {
final Element element = report.get(field);
final String value = element != null ? TextUtils.join("\n", element.flatten()) : null;
if (mMapping == null || mMapping.get(field) == null) {
finalReport.put(field.toString(), value);
} else {
finalReport.put(mMapping.get(field), value);
}
}
return finalReport;
}
private boolean isNull(@Nullable String aString) {
return aString == null || ACRAConstants.NULL_VALUE.equals(aString);
}
}