/*
* Copyright 2011 Luke Usherwood.
*
* 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 2.1 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, see <http://www.gnu.org/licenses/>.
*/
package net.bettyluke.tracinstant.download;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractListModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;
import net.bettyluke.tracinstant.data.Ticket;
import net.bettyluke.tracinstant.download.AttachmentCounter.CountCallback;
import net.bettyluke.tracinstant.download.Downloadable.FileDownloadable;
import net.bettyluke.tracinstant.download.Downloadable.TracDownloadable;
public class DownloadModel {
public final class ListModelView extends AbstractListModel<Target> {
int oldSize = 0;
@Override
public int getSize() {
return targets.size();
}
@Override
public Target getElementAt(int index) {
return targets.get(index);
}
public void modifiedElementAt(int index) {
fireContentsChanged(this, index, index);
}
public void allChanged() {
int newSize = targets.size();
if (oldSize > newSize) {
fireIntervalRemoved(this, newSize, oldSize - 1);
if (newSize > 0) {
fireContentsChanged(this, 0, newSize - 1);
}
} else {
if (oldSize < newSize) {
fireIntervalAdded(this, oldSize + 1, newSize - 1);
}
if (oldSize > 0) {
fireContentsChanged(this, 0, oldSize - 1);
}
}
oldSize = newSize;
}
}
public enum State {
IDLE, COUNTING, DOWNLOADING, CANCELLING
}
/** A list of event listeners for this component. */
private final EventListenerList listenerList = new EventListenerList();
private final ChangeEvent changeEvent = new ChangeEvent(this);
private final List<Target> targets = new ArrayList<>();
private final ListModelView listModel = new ListModelView();
private Path bugsDir;
private State state = State.IDLE;
private AttachmentDownloader tracDownloader = null;
private AttachmentDownloader fileDownloader = null;
public Path getBugsFolder() {
return bugsDir;
}
public void setBugsFolder(File bugsFolder) {
this.bugsDir = bugsFolder.toPath();
for (Target target : targets) {
target.setTopFolder(this.bugsDir);
}
fireStateChanged();
}
public void addAll(List<? extends Downloadable> attachments) {
for (Downloadable att : attachments) {
targets.add(new Target(bugsDir, att));
}
fireStateChanged();
}
public ListModelView getListModel() {
return listModel;
}
public void download() {
setState(State.DOWNLOADING);
Runnable doneRunner = new Runnable() {
int countdown = 2;
@Override
public void run() {
if (--countdown == 0) {
System.out.println("Downloader DONE");
setState(State.IDLE);
tracDownloader = null;
fileDownloader = null;
}
}
};
tracDownloader = new AttachmentDownloader(this, doneRunner);
fileDownloader = new AttachmentDownloader(this, doneRunner);
for (Target target : targets) {
if (!target.isSelected()) {
continue;
}
Downloadable source = target.getSource();
if (source instanceof TracDownloadable) {
tracDownloader.add(target);
} else if (source instanceof FileDownloadable) {
fileDownloader.add(target);
} else {
System.err.println("Unexpected source type: " + source);
}
}
tracDownloader.execute();
fileDownloader.execute();
}
public void cancelDownload() {
if (tracDownloader != null) {
assert state == State.DOWNLOADING;
setState(State.CANCELLING);
System.out.println("cancel TracDownloader");
// Subtle: the fileDownloader can BECOME null during this call if it was
// already complete, and the state will then become IDLE.
tracDownloader.cancel(true);
}
if (fileDownloader != null) {
assert state == State.DOWNLOADING;
setState(State.CANCELLING);
System.out.println("cancel fileDownloader");
fileDownloader.cancel(true);
}
}
public void setTargetState(Target target, Target.State newState) {
if (newState == target.getState()) {
return;
}
target.setState(newState);
if (newState == Target.State.ENDED || newState == Target.State.ERROR) {
if (newState == Target.State.ERROR) {
System.err.println(target.getErrorMessage()); // TODO: Show somehow?
} else {
target.setSelected(false);
}
if (countComplete() == targets.size()) {
setState(State.IDLE);
}
}
fireStateChanged();
}
public int countComplete() {
int complete = 0;
for (Target t : targets) {
if (t.getState() == Target.State.ENDED) {
++complete;
}
}
return complete;
}
/**
* Adds a <code>ChangeListener</code> to the list that is notified each time the model has
* changed.
*/
public void addChangeListener(ChangeListener l) {
listenerList.add(ChangeListener.class, l);
}
/**
* Removes a <code>ChangeListener</code> from the list that's notified each time the model has
* changed.
*/
public void removeChangeListener(ChangeListener l) {
listenerList.remove(ChangeListener.class, l);
}
/**
* Returns an array of all the <code>ChangeListener</code>s
*/
public ChangeListener[] getChangeListeners() {
return listenerList.getListeners(ChangeListener.class);
}
public void fireStateChanged() {
listModel.allChanged();
Object[] listeners = listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ChangeListener.class) {
((ChangeListener) listeners[i + 1]).stateChanged(changeEvent);
}
}
}
public boolean isBusy() {
return state != State.IDLE;
}
public boolean isDownloading() {
return state == State.DOWNLOADING;
}
public int getNumDownloads() {
return targets.size();
}
public String getDownloadSummary() {
int num = getNumDownloads();
switch (state) {
case COUNTING:
// Will be displayed with a busy icon, no need to add "..." (for eg)
return (num > 0) ? Integer.toString(num) : "";
case DOWNLOADING:
case CANCELLING:
return countComplete() + " / " + countFilesToDownloadOrDownloaded();
case IDLE:
break;
}
return "" + num + " ";
}
public void count(Ticket[] tickets) {
// Simple dumb implementation. Ignore all count requests unless idle. Once idle,
// user will need to change something to cause a recount. Should be fine.
if (state == State.DOWNLOADING || state == State.CANCELLING) {
return;
}
AttachmentCounter.restartCounting(tickets, new CountCallback() {
@Override
public void restart() {
targets.clear();
setState(State.COUNTING);
}
@Override
public void downloadsFound(List<? extends Downloadable> attachments) {
addAll(attachments);
}
@Override
public void done() {
setState(State.IDLE);
}
});
}
protected final void setState(State newState) {
if (state != newState) {
state = newState;
fireStateChanged();
}
}
public State getState() {
return state;
}
public int countFilesToOverwrite() {
int count = 0;
for (Target target : targets) {
if (target.isSelected() && target.isOverwriting()) {
count++;
}
}
return count;
}
public int countSelected() {
int count = 0;
for (Target target : targets) {
if (target.isSelected()) {
count++;
}
}
return count;
}
public int countFilesToDownloadOrDownloaded() {
int count = 0;
for (Target target : targets) {
if (target.isSelected() || target.getState() == Target.State.ENDED) {
count++;
}
}
return count;
}
public Path getAbsolutePath(Target target) {
Downloadable source = target.getSource();
return bugsDir.resolve("" + source.getTicketNumber()).resolve(source.getRelativePath());
}
}