blob: a72b4e14ae600f883b2c2d1bbbd02601dbea7e9c [file] [log] [blame]
/**
* Copyright (c) 2015 Codetrails GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.epp.logging.aeri.core.util;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.text.MessageFormat.format;
import static org.apache.commons.lang3.StringUtils.*;
import static org.eclipse.epp.logging.aeri.core.Constants.HIDDEN;
import static org.eclipse.epp.logging.aeri.core.IModelPackage.Literals.*;
import static org.eclipse.epp.logging.aeri.core.l10n.LogMessages.WARN_CYCLIC_EXCEPTION;
import static org.eclipse.epp.logging.aeri.core.util.Logs.log;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.EMap;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.epp.logging.aeri.core.Constants;
import org.eclipse.epp.logging.aeri.core.IBundle;
import org.eclipse.epp.logging.aeri.core.IModelFactory;
import org.eclipse.epp.logging.aeri.core.IReport;
import org.eclipse.epp.logging.aeri.core.IStackTraceElement;
import org.eclipse.epp.logging.aeri.core.IStatus;
import org.eclipse.epp.logging.aeri.core.IThrowable;
import org.eclipse.jdt.annotation.Nullable;
import org.osgi.framework.Bundle;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
@SuppressWarnings("null")
public class Reports {
public static Hasher newHasher() {
return Hashing.murmur3_128().newHasher();
}
public static IReport newReport(org.eclipse.core.runtime.IStatus event) {
checkNotNull(event);
IReport mReport = IModelFactory.eINSTANCE.createReport();
mReport.setJavaRuntimeVersion(SystemUtils.JAVA_RUNTIME_VERSION);
mReport.setEclipseBuildId(System.getProperty("eclipse.buildId", "-"));
mReport.setEclipseProduct(System.getProperty("eclipse.product", "-"));
mReport.setOsgiArch(System.getProperty("osgi.arch", "-"));
mReport.setOsgiWs(System.getProperty("osgi.ws", "-"));
mReport.setOsgiOs(System.getProperty(org.osgi.framework.Constants.FRAMEWORK_OS_NAME, "-"));
mReport.setOsgiOsVersion(System.getProperty(org.osgi.framework.Constants.FRAMEWORK_OS_VERSION, "-"));
mReport.setStatus(newStatus(event));
includeBundles(mReport);
return mReport;
}
private static void includeBundles(IReport report) {
checkNotNull(report);
StackTracePackagesCollector v = new StackTracePackagesCollector();
visit(report, v);
Set<String> uniqueBundleNames = Sets.newHashSet();
for (String packageName : v.packages) {
while (packageName.contains(".")) {
org.osgi.framework.Bundle guessedBundleForPackageName = Platform.getBundle(packageName);
packageName = StringUtils.substringBeforeLast(packageName, ".");
if (guessedBundleForPackageName != null) {
if (uniqueBundleNames.add(guessedBundleForPackageName.getSymbolicName())) {
IBundle mBundle = newBundle(guessedBundleForPackageName);
report.getPresentBundles().add(mBundle);
}
continue;
}
}
}
}
@VisibleForTesting
public static IStatus newStatus(org.eclipse.core.runtime.IStatus status) {
checkNotNull(status);
IStatus mStatus = IModelFactory.eINSTANCE.createStatus();
mStatus.setMessage(removeSourceFileContents(status.getMessage()));
mStatus.setSeverity(status.getSeverity());
mStatus.setCode(status.getCode());
mStatus.setPluginId(status.getPlugin());
org.osgi.framework.Bundle bundle = Platform.getBundle(status.getPlugin());
if (bundle != null) {
mStatus.setPluginVersion(bundle.getVersion().toString());
}
List<IStatus> mChildren = mStatus.getChildren();
Throwable exception = status.getException();
// CoreException handling
for (Throwable cur = exception; cur != null; cur = cur.getCause()) {
if (cur instanceof CoreException) {
CoreException coreException = (CoreException) cur;
org.eclipse.core.runtime.IStatus coreExceptionStatus = coreException.getStatus();
IStatus mCoreExceptionStatus = newStatus(coreExceptionStatus);
String detachedMessage = format("{0} [detached from CoreException of Status ''{1}'' by Error Reporting]",
mCoreExceptionStatus.getMessage(), mStatus.getMessage());
mCoreExceptionStatus.setMessage(detachedMessage);
mChildren.add(mCoreExceptionStatus);
// further CoreExceptions are handled in the detached Status
break;
}
}
// Multistatus handling
for (org.eclipse.core.runtime.IStatus child : status.getChildren()) {
mChildren.add(newStatus(child));
}
// some stacktraces from ui.monitoring should be filtered
boolean needFiltering = "org.eclipse.ui.monitoring".equals(status.getPlugin()) && (status.getCode() == 0 || status.getCode() == 1);
if (needFiltering) {
MultiStatusFilter.filter(mStatus);
}
if (exception != null) {
IThrowable mException = newThrowable(exception);
mStatus.setException(mException);
}
mStatus.setFingerprint(newStatusFingerprint(mStatus));
return mStatus;
}
/**
* Computes the exact fingerprint of the given status object. Two statuses have the same fingerprint only iff they have the same plug-in
* ids, plug-in versions, and messages (including its children statuses) and the exact same exceptions.
*/
public static String newStatusFingerprint(IStatus mStatus) {
checkNotNull(mStatus);
final Hasher hasher = newHasher();
ModelSwitch<Hasher> s = new ModelSwitch<Hasher>() {
@Override
public Hasher caseStatus(IStatus object) {
hasher.putString(stripToEmpty(object.getPluginId()), UTF_8);
hasher.putString(stripToEmpty(object.getPluginVersion()), UTF_8);
hasher.putString(stripToEmpty(object.getMessage()), UTF_8);
hasher.putInt(object.getSeverity());
hasher.putInt(object.getCode());
return null;
}
@Override
public Hasher caseStackTraceElement(IStackTraceElement object) {
hasher.putString(stripToEmpty(object.getClassName()), UTF_8);
hasher.putString(stripToEmpty(object.getMethodName()), UTF_8);
hasher.putInt(object.getLineNumber());
return null;
}
@Override
public Hasher caseThrowable(IThrowable object) {
hasher.putString(stripToEmpty(object.getClassName()), UTF_8);
hasher.putString(stripToEmpty(object.getMessage()), UTF_8);
return null;
}
};
visit(mStatus, s);
String hash = hasher.hash().toString();
return hash;
}
private static String removeSourceFileContents(@Nullable String message) {
message = defaultString(message);
if (message.contains(Constants.SOURCE_BEGIN_MESSAGE)) {
return Constants.SOURCE_FILE_REMOVED;
} else {
return message;
}
}
public static IThrowable newThrowable(Throwable throwable) {
checkNotNull(throwable);
IThrowable mThrowable = IModelFactory.eINSTANCE.createThrowable();
mThrowable.setMessage(throwable.getMessage());
mThrowable.setClassName(throwable.getClass().getName());
List<IStackTraceElement> mStackTrace = mThrowable.getStackTrace();
for (StackTraceElement stackTraceElement : throwable.getStackTrace()) {
IStackTraceElement mStackTraceElement = newStackTraceElement(stackTraceElement);
mStackTrace.add(mStackTraceElement);
}
Throwable cause = throwable.getCause();
if (cause != null) {
if (cause == throwable) {
log(WARN_CYCLIC_EXCEPTION, cause.toString());
return mThrowable;
}
mThrowable.setCause(newThrowable(cause));
}
return mThrowable;
}
/**
* Returns a hash of the given IThrowable. The hash may exclude the throwables' messages and the stack trace elements' line numbers if
* the respective parameters are set to false.
*/
public static String newThrowableFingerprint(IThrowable throwable, final boolean includeMessages, final boolean includeLineNumbers) {
checkNotNull(throwable);
final Hasher hasher = newHasher();
ModelSwitch<Hasher> s = new ModelSwitch<Hasher>() {
@Override
public Hasher caseThrowable(IThrowable object) {
hasher.putString(stripToEmpty(object.getClassName()), UTF_8);
if (includeMessages) {
hasher.putString(stripToEmpty(object.getMessage()), UTF_8);
}
return null;
}
@Override
public Hasher caseStackTraceElement(IStackTraceElement object) {
hasher.putString(stripToEmpty(object.getClassName()), UTF_8);
hasher.putString(stripToEmpty(object.getMethodName()), UTF_8);
if (includeLineNumbers) {
hasher.putInt(object.getLineNumber());
}
return null;
}
};
visit(throwable, s);
String hash = hasher.hash().toString();
return hash;
}
public static IStackTraceElement newStackTraceElement(StackTraceElement stackTraceElement) {
checkNotNull(stackTraceElement);
IStackTraceElement mStackTraceElement = IModelFactory.eINSTANCE.createStackTraceElement();
mStackTraceElement.setClassName(defaultString(stackTraceElement.getClassName(), Constants.MISSING));
mStackTraceElement.setMethodName(defaultString(stackTraceElement.getMethodName(), Constants.MISSING));
mStackTraceElement.setFileName(stackTraceElement.getFileName());
mStackTraceElement.setLineNumber(stackTraceElement.getLineNumber());
mStackTraceElement.setNative(stackTraceElement.isNativeMethod());
return mStackTraceElement;
}
public static IBundle newBundle(Bundle bundle) {
checkNotNull(bundle);
IBundle mBundle = IModelFactory.eINSTANCE.createBundle();
mBundle.setName(bundle.getSymbolicName());
mBundle.setVersion(bundle.getVersion().toString());
return mBundle;
}
public static IReport copy(IReport report) {
checkNotNull(report);
return EcoreUtil.copy(report);
}
public static IStatus copy(IStatus status) {
checkNotNull(status);
return EcoreUtil.copy(status);
}
public static IThrowable copy(IThrowable throwable) {
checkNotNull(throwable);
return EcoreUtil.copy(throwable);
}
public static IStackTraceElement copy(IStackTraceElement stackTraceElement) {
checkNotNull(stackTraceElement);
return EcoreUtil.copy(stackTraceElement);
}
public static IBundle copy(IBundle bundle) {
checkNotNull(bundle);
return EcoreUtil.copy(bundle);
}
public static String toPrettyString(IReport report) {
return toPrettyString(report, new HashMap<>());
}
public static String toPrettyString(IReport report, Map<String, String> reportProcessorIdsToReadable) {
checkNotNull(report);
PrettyPrintVisitor prettyPrintVisitor = new PrettyPrintVisitor(reportProcessorIdsToReadable);
visit(report, prettyPrintVisitor);
return prettyPrintVisitor.print();
}
public static <T, K extends EObject> T visit(K object, ModelSwitch<T> s) {
checkNotNull(object);
checkNotNull(s);
T t = s.doSwitch(object);
if (t != null) {
return t;
}
TreeIterator<EObject> allContents = EcoreUtil.getAllContents(object, true);
for (TreeIterator<EObject> iterator = allContents; iterator.hasNext();) {
EObject modelElement = iterator.next();
t = s.doSwitch(modelElement);
if (t != null) {
return t;
}
}
return null;
}
private static final class AnonymizeStackTracesSwitch extends ModelSwitch<Void> {
private final List<Pattern> regexes;
private AnonymizeStackTracesSwitch(List<Pattern> regexes) {
checkNotNull(regexes);
this.regexes = regexes;
}
@Override
public Void caseThrowable(IThrowable object) {
checkNotNull(object);
for (Pattern regex : regexes) {
if (regex.matcher(object.getClassName()).matches()) {
return null;
}
}
object.setClassName(HIDDEN);
return null;
}
@Override
public Void caseStackTraceElement(IStackTraceElement element) {
checkNotNull(element);
String input = element.getClassName() + "." + element.getMethodName();
for (Pattern regex : regexes) {
if (regex.matcher(input).matches()) {
return null;
}
}
// if no matcher matched, replace the frame with HIDDEN.HIDDEN:
element.setClassName(HIDDEN);
element.setMethodName(HIDDEN);
element.setFileName(HIDDEN);
element.setLineNumber(-1);
return null;
}
}
private static final class StackTracePackagesCollector extends ModelSwitch<Object> {
public TreeSet<String> packages = Sets.newTreeSet();
@Override
public Object caseStackTraceElement(IStackTraceElement element) {
checkNotNull(element);
String pkg = replace(substringBeforeLast(element.getClassName(), "."), ".internal.", ".");
packages.add(pkg);
return null;
}
}
public static void anonymizeMessages(IReport report) {
checkNotNull(report);
visit(report, new ModelSwitch<Void>() {
@Override
public Void caseThrowable(IThrowable object) {
checkNotNull(object);
if (object.eIsSet(THROWABLE__MESSAGE)) {
object.setMessage(HIDDEN);
}
return null;
}
@Override
public Void caseStatus(IStatus object) {
checkNotNull(object);
if (object.eIsSet(STATUS__MESSAGE)) {
object.setMessage(HIDDEN);
}
return null;
}
});
}
public static void anonymizeStackTraces(IReport report, final List<Pattern> regexes) {
checkNotNull(report);
checkNotNull(regexes);
visit(report, new AnonymizeStackTracesSwitch(regexes));
}
public static void anonymizeStackTraces(IThrowable throwable, final List<Pattern> regexes) {
checkNotNull(throwable);
checkNotNull(regexes);
visit(throwable, new AnonymizeStackTracesSwitch(regexes));
}
public static void anonymizeStackTraceElements(IStackTraceElement element, final List<Pattern> regexes) {
visit(element, new AnonymizeStackTracesSwitch(regexes));
}
private static class MultiStatusFilter {
public static void filter(IStatus status) {
checkNotNull(status);
HashSet<IThrowable> throwables = new HashSet<>();
filter(status, throwables);
}
private static void filter(IStatus status, Set<IThrowable> throwables) {
EList<IStatus> children = status.getChildren();
int removedCount = 0;
for (int i = children.size() - 1; i >= 0; i--) {
IStatus childStatus = children.get(i);
if (filterChild(childStatus, throwables)) {
children.remove(i);
removedCount++;
} else {
filter(childStatus, throwables);
}
}
if (removedCount > 0) {
status.setMessage(
String.format("%s [%d child-status duplicates removed by Error Reporting]", status.getMessage(), removedCount));
}
}
private static boolean filterChild(IStatus status, Set<IThrowable> throwables) {
IThrowable throwable = status.getException();
if (throwable.getStackTrace().isEmpty()) {
return true;
}
for (IThrowable t : throwables) {
if (stackTraceMatches(throwable, t)) {
return true;
}
}
throwables.add(throwable);
return false;
}
private static boolean stackTraceMatches(IThrowable throwable, IThrowable t) {
EList<IStackTraceElement> stackTrace = throwable.getStackTrace();
EList<IStackTraceElement> stackTrace2 = t.getStackTrace();
if (stackTrace.size() != stackTrace2.size()) {
return false;
}
for (int i = 0; i < stackTrace.size(); i++) {
IStackTraceElement ste = stackTrace.get(i);
IStackTraceElement ste2 = stackTrace2.get(i);
if (!classNameAndMethodNameEqual(ste, ste2)) {
return false;
}
}
return true;
}
private static boolean classNameAndMethodNameEqual(IStackTraceElement ste, IStackTraceElement ste2) {
return ste.getClassName().equals(ste2.getClassName()) && ste.getMethodName().equals(ste2.getMethodName());
}
}
private static class PrettyPrintVisitor extends ModelSwitch<Object> {
private static final int RIGHT_PADDING = 20;
private StringBuilder reportStringBuilder = new StringBuilder();
private StringBuilder statusStringBuilder = new StringBuilder();
private StringBuilder bundlesStringBuilder = new StringBuilder();
private StringBuilder auxiliaryInformationStringBuilder = new StringBuilder();
private Map<String, String> directiveToReadable;
PrettyPrintVisitor(Map<String, String> reportProcessorIdsToReadable) {
this.directiveToReadable = reportProcessorIdsToReadable;
appendHeadline("BUNDLES", bundlesStringBuilder);
}
private void appendAttributes(EObject object, StringBuilder builder) {
for (EAttribute attribute : object.eClass().getEAllAttributes()) {
// String format = "%-" + RIGHT_PADDING + "s%s\n";
// String line = String.format(format, attribute.getName(), value);
Object value = firstNonNull(object.eGet(attribute), "");
builder.append(StringUtils.rightPad(attribute.getName(), RIGHT_PADDING));
builder.append(value);
builder.append('\n');
}
builder.append("\n");
}
private void appendHeadline(String headline, StringBuilder builder) {
if (builder.length() != 0) {
builder.append("\n");
}
String line = headline.replaceAll(".", "-") + "\n";
builder.append(line);
builder.append(headline + "\n");
builder.append(line);
}
@Override
public Object caseReport(IReport report) {
checkNotNull(report);
appendHeadline("REPORT", reportStringBuilder);
appendAttributes(report, reportStringBuilder);
EMap<String, String> auxiliaryInformation = report.getAuxiliaryInformation();
appendProvidedInformation(auxiliaryInformation, auxiliaryInformationStringBuilder);
return null;
}
private void appendProvidedInformation(EMap<String, String> providedInformation, StringBuilder builder) {
for (Entry<String, String> entry : providedInformation.entrySet()) {
String processorId = entry.getKey();
if (directiveToReadable.containsKey(processorId)) {
processorId = directiveToReadable.get(processorId);
}
String information = entry.getValue();
appendHeadline(processorId, auxiliaryInformationStringBuilder);
builder.append(information);
}
}
@Override
public Object caseStatus(IStatus status) {
checkNotNull(status);
appendHeadline("STATUS", statusStringBuilder);
appendAttributes(status, statusStringBuilder);
IThrowable exception = status.getException();
if (exception != null) {
statusStringBuilder.append("Exception:");
append(exception, statusStringBuilder);
}
return null;
}
private void append(IThrowable throwable, StringBuilder builder) {
builder.append(String.format("%s: %s\n", throwable.getClassName(), throwable.getMessage()));
for (IStackTraceElement element : throwable.getStackTrace()) {
builder.append(String.format("\t at %s.%s(%s:%s)\n", element.getClassName(), element.getMethodName(), element.getFileName(),
element.getLineNumber()));
}
IThrowable cause = throwable.getCause();
if (cause != null) {
statusStringBuilder.append("Caused by: ");
append(cause, builder);
}
}
@Override
public Object caseBundle(IBundle bundle) {
checkNotNull(bundle);
appendAttributes(bundle, bundlesStringBuilder);
return null;
}
public String print() {
return new StringBuilder().append(statusStringBuilder).append("\n").append(reportStringBuilder).append(bundlesStringBuilder)
.append("\n").append(auxiliaryInformationStringBuilder).toString();
}
}
}