blob: e0c7b6802a9a1b2a7d52179a94e70490ded957fa [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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 com.android.ide.common.res2;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.blame.Message;
import com.android.ide.common.blame.Message.Kind;
import com.android.ide.common.blame.SourceFile;
import com.android.ide.common.blame.SourceFilePosition;
import com.android.ide.common.blame.SourcePosition;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.xml.sax.SAXParseException;
import java.io.File;
import java.util.Collection;
import java.util.List;
/**
* Exception for errors during merging.
*/
public class MergingException extends Exception {
public static final String MULTIPLE_ERRORS = "Multiple errors:";
@NonNull
private final List<Message> mMessages;
/**
* For internal use. Creates a new MergingException
*
* @param cause the original exception. May be null.
* @param messages the messaged. Must contain at least one item.
*/
protected MergingException(@Nullable Throwable cause, @NonNull Message... messages) {
super(messages.length == 1 ? messages[0].getText() : MULTIPLE_ERRORS, cause);
mMessages = ImmutableList.copyOf(messages);
}
public static class Builder {
@Nullable
private Throwable mCause = null;
@Nullable
private String mMessageText = null;
@Nullable
private String mOriginalMessageText = null;
@NonNull
private SourceFile mFile = SourceFile.UNKNOWN;
@NonNull
private SourcePosition mPosition = SourcePosition.UNKNOWN;
private Builder() {
}
public Builder wrapException(@NonNull Throwable cause) {
mCause = cause;
mOriginalMessageText = Throwables.getStackTraceAsString(cause);
return this;
}
public Builder withFile(@NonNull File file) {
mFile = new SourceFile(file);
return this;
}
public Builder withFile(@NonNull SourceFile file) {
mFile = file;
return this;
}
public Builder withPosition(@NonNull SourcePosition position) {
mPosition = position;
return this;
}
public Builder withMessage(@NonNull String messageText, Object... args) {
mMessageText = args.length == 0 ? messageText : String.format(messageText, args);
return this;
}
public MergingException build() {
if (mCause != null) {
if (mMessageText == null) {
mMessageText = Objects.firstNonNull(
mCause.getLocalizedMessage(), mCause.getClass().getCanonicalName());
}
if (mPosition == SourcePosition.UNKNOWN && mCause instanceof SAXParseException) {
SAXParseException exception = (SAXParseException) mCause;
int lineNumber = exception.getLineNumber();
if (lineNumber != -1) {
// Convert positions to be 0-based for SourceFilePosition.
mPosition = new SourcePosition(lineNumber - 1,
exception.getColumnNumber() - 1, -1);
}
}
}
if (mMessageText == null) {
mMessageText = "Unknown error.";
}
return new MergingException(
mCause,
new Message(
Kind.ERROR,
mMessageText,
Objects.firstNonNull(mOriginalMessageText, mMessageText),
new SourceFilePosition(mFile, mPosition)));
}
}
public static Builder wrapException(@NonNull Throwable cause) {
return new Builder().wrapException(cause);
}
public static Builder withMessage(@NonNull String message, Object... args) {
return new Builder().withMessage(message, args);
}
public static void throwIfNonEmpty(Collection<Message> messages) throws MergingException {
if (!messages.isEmpty()) {
throw new MergingException(null, Iterables.toArray(messages, Message.class));
}
}
@NonNull
public List<Message> getMessages() {
return mMessages;
}
/**
* Computes the error message to display for this error
*/
@NonNull
@Override
public String getMessage() {
List<String> messages = Lists.newArrayListWithCapacity(mMessages.size());
for (Message message : mMessages) {
StringBuilder sb = new StringBuilder();
List<SourceFilePosition> sourceFilePositions = message.getSourceFilePositions();
if (sourceFilePositions.size() > 1 || !sourceFilePositions.get(0)
.equals(SourceFilePosition.UNKNOWN)) {
sb.append(Joiner.on('\t').join(sourceFilePositions));
}
String text = message.getText();
if (sb.length() > 0) {
sb.append(':').append(' ');
// ALWAYS insert the string "Error:" between the path and the message.
// This is done to make the error messages more simple to detect
// (since a generic path: message pattern can match a lot of output, basically
// any labeled output, and we don't want to do file existence checks on any random
// string to the left of a colon.)
if (!text.startsWith("Error: ")) {
sb.append("Error: ");
}
} else if (!text.contains("Error: ")) {
sb.append("Error: ");
}
// If the error message already starts with the path, strip it out.
// This avoids redundant looking error messages you can end up with
// like for example for permission denied errors where the error message
// string itself contains the path as a prefix:
// /my/full/path: /my/full/path (Permission denied)
if (sourceFilePositions.size() == 1) {
File file = sourceFilePositions.get(0).getFile().getSourceFile();
if (file != null) {
String path = file.getAbsolutePath();
if (text.startsWith(path)) {
int stripStart = path.length();
if (text.length() > stripStart && text.charAt(stripStart) == ':') {
stripStart++;
}
if (text.length() > stripStart && text.charAt(stripStart) == ' ') {
stripStart++;
}
text = text.substring(stripStart);
}
}
}
sb.append(text);
messages.add(sb.toString());
}
return Joiner.on('\n').join(messages);
}
@Override
public String toString() {
return getMessage();
}
}