blob: 44fad3c841c916b4933083964bfde643d3209a65 [file] [log] [blame]
/*
* Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.jersey.internal;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.Logger;
import org.glassfish.jersey.Severity;
import org.glassfish.jersey.internal.util.Producer;
/**
* Errors utility used to file processing messages (e.g. validation, provider, resource building errors, hint).
* <p/>
* Error filing methods ({@code #warning}, {@code #error}, {@code #fatal}) can be invoked only in the "error scope" which is
* created by {@link #process(Producer)} or
* {@link #processWithException(Producer)} methods. Filed error messages are present also in this
* scope.
* <p/>
* TODO do not use static thread local?
*
* @author Michal Gajdos
*/
public class Errors {
private static final Logger LOGGER = Logger.getLogger(Errors.class.getName());
private static final ThreadLocal<Errors> errors = new ThreadLocal<Errors>();
/**
* Add an error to the list of messages.
*
* @param message message of the error.
* @param severity indicates severity of added error.
*/
public static void error(final String message, Severity severity) {
error(null, message, severity);
}
/**
* Add an error to the list of messages.
*
* @param source source of the error.
* @param message message of the error.
* @param severity indicates severity of added error.
*/
public static void error(final Object source, final String message, final Severity severity) {
getInstance().issues.add(new ErrorMessage(source, message, severity));
}
/**
* Add a fatal error to the list of messages.
*
* @param source source of the error.
* @param message message of the error.
*/
public static void fatal(final Object source, final String message) {
error(source, message, Severity.FATAL);
}
/**
* Add a warning to the list of messages.
*
* @param source source of the error.
* @param message message of the error.
*/
public static void warning(final Object source, final String message) {
error(source, message, Severity.WARNING);
}
/**
* Add a hint to the list of messages.
*
* @param source source of the error.
* @param message message of the error.
*/
public static void hint(final Object source, final String message) {
getInstance().issues.add(new ErrorMessage(source, message, Severity.HINT));
}
/**
* Log errors and throw an exception if there are any fatal issues detected and
* the {@code throwException} flag has been set to {@code true}.
*
* @param throwException if set to {@code true}, any fatal issues will cause a {@link ErrorMessagesException}
* to be thrown.
*/
private static void processErrors(final boolean throwException) {
final List<ErrorMessage> errors = new ArrayList<ErrorMessage>(Errors.errors.get().issues);
boolean isFatal = logErrors(errors);
if (throwException && isFatal) {
throw new ErrorMessagesException(errors);
}
}
/**
* Log errors and return a status flag indicating whether a fatal issue has been found
* in the error collection.
* <p>
* The {@code afterMark} flag indicates whether only those issues should be logged that were
* added after a {@link #mark() mark has been set}.
* </p>
*
* @param afterMark if {@code true}, only issues added after a mark has been set are returned,
* if {@code false} all issues are returned.
* @return {@code true} if there are any fatal issues present in the collection, {@code false}
* otherwise.
*/
public static boolean logErrors(final boolean afterMark) {
return logErrors(getInstance()._getErrorMessages(afterMark));
}
/**
* Log supplied errors and return a status flag indicating whether a fatal issue has been found
* in the error collection.
*
* @param errors a collection of errors to be logged.
* @return {@code true} if there are any fatal issues present in the collection, {@code false}
* otherwise.
*/
private static boolean logErrors(final Collection<ErrorMessage> errors) {
boolean isFatal = false;
if (!errors.isEmpty()) {
StringBuilder fatals = new StringBuilder("\n");
StringBuilder warnings = new StringBuilder();
StringBuilder hints = new StringBuilder();
for (final ErrorMessage error : errors) {
switch (error.getSeverity()) {
case FATAL:
isFatal = true;
fatals.append(LocalizationMessages.ERROR_MSG(error.getMessage())).append('\n');
break;
case WARNING:
warnings.append(LocalizationMessages.WARNING_MSG(error.getMessage())).append('\n');
break;
case HINT:
hints.append(LocalizationMessages.HINT_MSG(error.getMessage())).append('\n');
break;
}
}
if (isFatal) {
LOGGER.severe(LocalizationMessages.ERRORS_AND_WARNINGS_DETECTED(fatals.append(warnings)
.append(hints).toString()));
} else {
if (warnings.length() > 0) {
LOGGER.warning(LocalizationMessages.WARNINGS_DETECTED(warnings.toString()));
}
if (hints.length() > 0) {
LOGGER.config(LocalizationMessages.HINTS_DETECTED(hints.toString()));
}
}
}
return isFatal;
}
/**
* Check whether a fatal error is present in the list of all messages.
*
* @return {@code true} if there are any fatal issues in this error context, {@code false} otherwise.
*/
public static boolean fatalIssuesFound() {
for (final ErrorMessage message : getInstance().issues) {
if (message.getSeverity() == Severity.FATAL) {
return true;
}
}
return false;
}
/**
* Invoke given producer task and gather errors.
* <p/>
* After the task is complete all gathered errors are logged. No exception is thrown
* even if there is a fatal error present in the list of errors.
*
* @param producer producer task to be invoked.
* @return the result produced by the task.
*/
public static <T> T process(final Producer<T> producer) {
return process(producer, false);
}
/**
* Invoke given callable task and gather messages.
* <p/>
* After the task is complete all gathered errors are logged. Any exception thrown
* by the throwable is re-thrown.
*
* @param task callable task to be invoked.
* @return the result produced by the task.
* @throws Exception exception thrown by the task.
*/
public static <T> T process(final Callable<T> task) throws Exception {
return process(task, true);
}
/**
* Invoke given producer task and gather messages.
* <p/>
* After the task is complete all gathered errors are logged. If there is a fatal error
* present in the list of errors an {@link ErrorMessagesException exception} is thrown.
*
* @param producer producer task to be invoked.
* @return the result produced by the task.
*/
public static <T> T processWithException(final Producer<T> producer) {
return process(producer, true);
}
/**
* Invoke given task and gather messages.
* <p/>
* After the task is complete all gathered errors are logged. No exception is thrown
* even if there is a fatal error present in the list of errors.
*
* @param task task to be invoked.
*/
public static void process(final Runnable task) {
process(new Producer<Void>() {
@Override
public Void call() {
task.run();
return null;
}
}, false);
}
/**
* Invoke given task and gather messages.
* <p/>
* After the task is complete all gathered errors are logged. If there is a fatal error
* present in the list of errors an {@link ErrorMessagesException exception} is thrown.
*
* @param task task to be invoked.
*/
public static void processWithException(final Runnable task) {
process(new Producer<Void>() {
@Override
public Void call() {
task.run();
return null;
}
}, true);
}
private static <T> T process(final Producer<T> task, final boolean throwException) {
try {
return process((Callable<T>) task, throwException);
} catch (RuntimeException ex) {
throw ex;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private static <T> T process(final Callable<T> task, final boolean throwException) throws Exception {
Errors instance = errors.get();
if (instance == null) {
instance = new Errors();
errors.set(instance);
}
instance.preProcess();
Exception caught = null;
try {
return task.call();
} catch (Exception re) {
// If a runtime exception is caught then report errors and rethrow.
caught = re;
} finally {
instance.postProcess(throwException && caught == null);
}
throw caught;
}
private static Errors getInstance() {
final Errors instance = errors.get();
// No error processing in scope
if (instance == null) {
throw new IllegalStateException(LocalizationMessages.NO_ERROR_PROCESSING_IN_SCOPE());
}
// The following should not be necessary but given the fragile nature of
// static thread local probably best to add it in case some internals of
// this class change
if (instance.stack == 0) {
errors.remove();
throw new IllegalStateException(LocalizationMessages.NO_ERROR_PROCESSING_IN_SCOPE());
}
return instance;
}
/**
* Get the list of all error messages.
*
* @return non-null error message list.
*/
public static List<ErrorMessage> getErrorMessages() {
return getErrorMessages(false);
}
/**
* Get the list of error messages.
* <p>
* The {@code afterMark} flag indicates whether only those issues should be returned that were
* added after a {@link #mark() mark has been set}.
* </p>
*
* @param afterMark if {@code true}, only issues added after a mark has been set are returned,
* if {@code false} all issues are returned.
* @return non-null error list.
*/
public static List<ErrorMessage> getErrorMessages(final boolean afterMark) {
return getInstance()._getErrorMessages(afterMark);
}
/**
* Set a mark at a current position in the errors messages list.
*/
public static void mark() {
getInstance()._mark();
}
/**
* Remove a previously set mark, if any.
*/
public static void unmark() {
getInstance()._unmark();
}
/**
* Removes all issues that have been added since the last marked position as well as
* removes the last mark.
*/
public static void reset() {
getInstance()._reset();
}
private final ArrayList<ErrorMessage> issues = new ArrayList<ErrorMessage>(0);
private Errors() {
}
private Deque<Integer> mark = new ArrayDeque<Integer>(4);
private int stack = 0;
private void _mark() {
mark.addLast(issues.size());
}
private void _unmark() {
mark.pollLast();
}
private void _reset() {
final Integer _pos = mark.pollLast(); // also performs "unmark" functionality
final int markedPos = (_pos == null) ? -1 : _pos;
if (markedPos >= 0 && markedPos < issues.size()) {
issues.subList(markedPos, issues.size()).clear();
}
}
private void preProcess() {
stack++;
}
private void postProcess(boolean throwException) {
stack--;
if (stack == 0) {
try {
if (!issues.isEmpty()) {
processErrors(throwException);
}
} finally {
errors.remove();
}
}
}
private List<ErrorMessage> _getErrorMessages(final boolean afterMark) {
if (afterMark) {
final Integer _pos = mark.peekLast();
final int markedPos = (_pos == null) ? -1 : _pos;
if (markedPos >= 0 && markedPos < issues.size()) {
return Collections.unmodifiableList(new ArrayList<ErrorMessage>(issues.subList(markedPos, issues.size())));
} // else return all errors
}
return Collections.unmodifiableList(new ArrayList<ErrorMessage>(issues));
}
/**
* Error message exception.
*/
public static class ErrorMessagesException extends RuntimeException {
private final List<ErrorMessage> messages;
private ErrorMessagesException(final List<ErrorMessage> messages) {
this.messages = messages;
}
/**
* Get encountered error messages.
*
* @return encountered error messages.
*/
public List<ErrorMessage> getMessages() {
return messages;
}
}
/**
* Generic error message.
*/
public static class ErrorMessage {
private final Object source;
private final String message;
private final Severity severity;
private ErrorMessage(final Object source, final String message, Severity severity) {
this.source = source;
this.message = message;
this.severity = severity;
}
/**
* Get {@link Severity}.
*
* @return severity of current {@code ErrorMessage}.
*/
public Severity getSeverity() {
return severity;
}
/**
* Human-readable description of the issue.
*
* @return message describing the issue.
*/
public String getMessage() {
return message;
}
/**
* The issue source.
* <p/>
* Identifies the object where the issue was found.
*
* @return source of the issue.
*/
public Object getSource() {
return source;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ErrorMessage that = (ErrorMessage) o;
if (message != null ? !message.equals(that.message) : that.message != null) {
return false;
}
if (severity != that.severity) {
return false;
}
if (source != null ? !source.equals(that.source) : that.source != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = source != null ? source.hashCode() : 0;
result = 31 * result + (message != null ? message.hashCode() : 0);
result = 31 * result + (severity != null ? severity.hashCode() : 0);
return result;
}
}
}