/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero 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
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.tools.logging;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.RandomAccess;
import java.util.logging.Level;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import com.rapidminer.gui.tools.ScaledImageIcon;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.tools.AbstractObservable;
import com.rapidminer.tools.I18N;
/**
* The abstract model for a log in the {@link LogViewer}.
* <p>
* <strong>Note: </strong>Do not extend this directly, but rather extend
* {@link AbstractPushLogModel} and {@link AbstractPullLogModel}.
* </p>
*
* @author Sabrina Kirstein, Marco Boeck, Marcel Michel
*/
public abstract class AbstractLogModel extends AbstractObservable<List<LogEntry>> implements LogModel {
private static final Object LOCK = new Object();
/**
* This code is in Public Domain. Please retain the author annotation.
*
* @author Isak du Preez (isak at du-preez dot com, www.du-preez.com)
*/
private class CircularArrayList<E> extends AbstractList<E> implements RandomAccess {
private final int n; // buffer length
private final List<E> buf; // a List implementing RandomAccess
private int head = 0;
private int tail = 0;
public CircularArrayList(int capacity) {
n = capacity + 1;
buf = new ArrayList<E>(Collections.nCopies(n, (E) null));
}
@SuppressWarnings("unused")
public int capacity() {
return n - 1;
}
private int wrapIndex(int i) {
int m = i % n;
if (m < 0) { // java modulus can be negative
m += n;
}
return m;
}
// This method is O(n) but will never be called if the
// CircularArrayList is used in its typical/intended role.
private void shiftBlock(int startIndex, int endIndex) {
assert endIndex > startIndex;
for (int i = endIndex - 1; i >= startIndex; i--) {
set(i + 1, get(i));
}
}
@Override
public int size() {
return tail - head + (tail < head ? n : 0);
}
@Override
public E get(int i) {
if (i < 0 || i >= size()) {
throw new IndexOutOfBoundsException();
}
return buf.get(wrapIndex(head + i));
}
@Override
public E set(int i, E e) {
if (i < 0 || i >= size()) {
throw new IndexOutOfBoundsException();
}
return buf.set(wrapIndex(head + i), e);
}
@Override
public void add(int i, E e) {
int s = size();
if (s == n - 1) {
throw new IllegalStateException("Cannot add element." + " CircularArrayList is filled to capacity.");
}
if (i < 0 || i > s) {
throw new IndexOutOfBoundsException();
}
tail = wrapIndex(tail + 1);
if (i < s) {
shiftBlock(i, s);
}
set(i, e);
}
@Override
public E remove(int i) {
int s = size();
if (i < 0 || i >= s) {
throw new IndexOutOfBoundsException();
}
E e = get(i);
if (i > 0) {
shiftBlock(0, i);
}
head = wrapIndex(head + 1);
return e;
}
}
/** the min and max size of an icon (if set) */
private static final int ICON_SIZE = 16;
/**
* the max size of log entries, if this size is exceeded the first elements will be dismissed
*/
public static final int DEFAULT_MAX_LOG_ENTRIES = 100_000;
/** icon which is used if a log specifies no own icon */
private static final ImageIcon FALLBACK_ICON = SwingTools
.createIcon("16/" + I18N.getGUIMessage("gui.logging.fallback.icon"));
/** log list */
private List<LogEntry> logs;
/** Icon */
private Icon modelIcon;
/** Unique Name */
private String modelName;
/** Log Mode */
private LogMode logMode;
/** is closable ? */
private boolean isClosable;
/** Log Level of the model */
private Level logLevel;
/**
* Creates a new log model with max {@value #DEFAULT_MAX_LOG_ENTRIES} log entries. If the size
* is exceeded old entries will be overwritten.
*
* @param modelIcon
* can be <code>null</code>. If not <code>null</code>, must be 16x16 pixel
* @param modelName
* cannot be <code>null</code> or empty. Must not exceed
* {@link LogModel#MAX_NAME_LENGTH} characters
* @param logMode
* see {@link LogMode#PULL} and {@link LogMode#PUSH}
* @param isClosable
* if <code>true</code>, the user can close the log via a button in the GUI
*/
public AbstractLogModel(Icon modelIcon, String modelName, LogMode logMode, boolean isClosable) {
this(modelIcon, modelName, logMode, isClosable, DEFAULT_MAX_LOG_ENTRIES);
}
/**
* Creates a new log model with the defined size of log entries. If the size is exceeded old
* entries will be overwritten.
*
* @param modelIcon
* can be <code>null</code>. If not <code>null</code>, must be 16x16 pixel
* @param modelName
* cannot be <code>null</code> or empty. Must not exceed
* {@link LogModel#MAX_NAME_LENGTH} characters
* @param logMode
* see {@link LogMode#PULL} and {@link LogMode#PUSH}
* @param isClosable
* if <code>true</code>, the user can close the log via a button in the GUI
* @param maxLogEntries
* the maximum size of log entries
*/
public AbstractLogModel(Icon modelIcon, String modelName, LogMode logMode, boolean isClosable, int maxLogEntries) {
if (modelName == null || "".equals(modelName.trim())) {
throw new IllegalArgumentException("modelName must not be null or empty!");
}
if (logMode == null) {
throw new IllegalArgumentException("logMode must not be null!");
}
// enforce max length of name to avoid ugly GUI
if (modelName.length() > MAX_NAME_LENGTH) {
throw new IllegalArgumentException("modelName must not exeeed " + MAX_NAME_LENGTH + " characters!");
}
// enforce correct icon size (if icon is set)
if (modelIcon != null) {
if (!(modelIcon instanceof ScaledImageIcon) && modelIcon.getIconHeight() != ICON_SIZE
|| modelIcon.getIconWidth() != ICON_SIZE) {
throw new IllegalArgumentException("if modelIcon is not null it must be 16x16 pixel!");
}
this.modelIcon = modelIcon;
} else {
this.modelIcon = FALLBACK_ICON;
}
// this is important as we read the entries in reverse order in the LogViewer
// with a LinkedList the performance would become abysmal for huge logs
// where ArrayList takes constant time
logs = new CircularArrayList<>(maxLogEntries);
this.modelName = modelName;
this.logMode = logMode;
this.isClosable = isClosable;
this.logLevel = Level.INFO;
}
@Override
public void addLogEntries(List<LogEntry> logEntries) {
if (logEntries != null) {
synchronized (LOCK) {
int diff = logs.size() + logEntries.size() - DEFAULT_MAX_LOG_ENTRIES;
while (diff > 0) {
logs.remove(0);
diff--;
}
logs.addAll(logEntries);
fireUpdate(logEntries);
}
}
}
@Override
public void clearLog() {
synchronized (LOCK) {
logs.clear();
fireUpdate();
}
}
@Override
public List<LogEntry> getLogEntries() {
return Collections.unmodifiableList(logs);
}
@Override
public Icon getIcon() {
return modelIcon;
}
@Override
public String getName() {
return modelName;
}
@Override
public LogMode getLogMode() {
return logMode;
}
@Override
public Level getLogLevel() {
return logLevel;
}
@Override
public void setLogLevel(Level level) {
this.logLevel = level;
}
@Override
public boolean isClosable() {
return isClosable;
}
@Override
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof LogModel) {
LogModel anotherModel = (LogModel) anObject;
if (!getName().equals(anotherModel.getName())) {
return false;
}
if (getLogEntries().size() != anotherModel.getLogEntries().size()) {
return false;
}
if (getLogMode() != anotherModel.getLogMode()) {
return false;
}
if (getLogLevel() == null && anotherModel.getLogLevel() != null
|| getLogLevel() != null && anotherModel.getLogLevel() == null) {
return false;
}
if (!getLogLevel().equals(anotherModel.getLogLevel())) {
return false;
}
for (int i = 0; i < getLogEntries().size(); i++) {
// size is identical as we have checked earlier
if (!getLogEntries().get(i).equals(anotherModel.getLogEntries().get(i))) {
return false;
}
}
return true;
}
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getLogEntries().hashCode();
result = prime * result + getName().hashCode();
if (getLogLevel() != null) {
result = prime * result + getLogLevel().hashCode();
}
result = prime * result + getLogMode().hashCode();
return result;
}
@Override
public String toString() {
String logLevel = getLogLevel() != null ? getLogLevel().toString() : "INFO";
return getName() + " - " + logLevel + " (" + getLogEntries().size() + " entries)";
}
}