/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.core.util;
import com.google.common.base.Joiner;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import java.util.Iterator;
import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;
/**
* A runtime exception that provides contextual information as a list of key-value
* pairs. This information is added to the message and stack trace.
* <p>
* Example:
* <pre>
* try {
*
* } catch (Exception e) {
* throw ContextException.of("Unable to assign issue", e)
* .addContext("projectUuid", "P1")
* .addContext("issueUuid", "I1")
* .addContext("login", "felix");
* }
* </pre>
* </p>
* <p>
* Contexts of nested {@link ContextException}s are merged:
* <pre>
* try {
* throw ContextException.of("Something wrong").addContext("login", "josette");
* } catch (Exception e) {
* throw ContextException.of("Unable to assign issue", e)
* .addContext("issueUuid", "I1")
* .addContext("login", "felix");
* }
* </pre>
* </p>
* <p>
* The generated message, usually written to a log with stack trace, looks like:
* <pre>
* Unable to assign issue | issueUuid=I1 | login=[josette,felix]
* </pre>
* </p>
*/
public class ContextException extends RuntimeException {
private static final Joiner COMMA_JOINER = Joiner.on(',');
// LinkedListMultimap is used to keep order of keys and values
private final transient ListMultimap<String, Object> context = LinkedListMultimap.create();
private ContextException(Throwable t) {
super(t);
}
private ContextException(String message, Throwable t) {
super(message, t);
}
private ContextException(String message) {
super(message);
}
private ContextException addContext(ContextException e) {
this.context.putAll(e.context);
return this;
}
public ContextException addContext(String key, @Nullable Object value) {
context.put(key, value);
return this;
}
public ContextException clearContext(String key) {
context.removeAll(key);
return this;
}
public ContextException setContext(String key, @Nullable Object value) {
clearContext(key);
return addContext(key, value);
}
/**
* Returns the values associated with {@code key}, if any, else returns an
* empty list.
*/
public List<Object> getContext(String key) {
return context.get(key);
}
public static ContextException of(Throwable t) {
if (t instanceof ContextException) {
return new ContextException(t.getCause()).addContext((ContextException) t);
}
return new ContextException(t);
}
public static ContextException of(String message, Throwable t) {
if (t instanceof ContextException) {
return new ContextException(message, t.getCause()).addContext((ContextException) t);
}
return new ContextException(message, t);
}
public static ContextException of(String message) {
return new ContextException(message);
}
@Override
@Nonnull
public String getMessage() {
return format(super.getMessage());
}
/**
* Provides the message explaining the exception without the contextual data.
*/
@CheckForNull
public String getRawMessage() {
return super.getMessage();
}
private String format(@Nullable String baseMessage) {
StringBuilder sb = new StringBuilder();
Iterator<String> keyIt = context.keySet().iterator();
if (StringUtils.isNotBlank(baseMessage)) {
sb.append(baseMessage);
if (keyIt.hasNext()) {
sb.append(" | ");
}
}
while (keyIt.hasNext()) {
String key = keyIt.next();
sb.append(key).append("=");
List<Object> values = getContext(key);
if (values.size() > 1) {
sb.append("[").append(COMMA_JOINER.join(values)).append("]");
} else if (values.size() == 1) {
sb.append(values.get(0));
}
if (keyIt.hasNext()) {
sb.append(" | ");
}
}
return sb.toString();
}
}