commit cd4f99df3dec637da6ab4de96fb78c0a24628826 Author: akwizgran Date: Tue Jun 21 18:01:28 2011 +0100 Initial commit with new directory structure. diff --git a/.classpath b/.classpath new file mode 100644 index 000000000..18ed19663 --- /dev/null +++ b/.classpath @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..10f676900 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/windows-jre +/Briar +/bin diff --git a/.project b/.project new file mode 100644 index 000000000..6c3dba207 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + briar-prototype + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..1cf045675 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Wed Apr 13 15:01:36 BST 2011 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/api/build.xml b/api/build.xml new file mode 100644 index 000000000..48ffec3d9 --- /dev/null +++ b/api/build.xml @@ -0,0 +1,3 @@ + + + diff --git a/api/net/sf/briar/api/crypto/Password.java b/api/net/sf/briar/api/crypto/Password.java new file mode 100644 index 000000000..be576f328 --- /dev/null +++ b/api/net/sf/briar/api/crypto/Password.java @@ -0,0 +1,6 @@ +package net.sf.briar.api.crypto; + +public interface Password { + + char[] getPassword(); +} diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java new file mode 100644 index 000000000..8f5cd228e --- /dev/null +++ b/api/net/sf/briar/api/db/DatabaseComponent.java @@ -0,0 +1,43 @@ +package net.sf.briar.api.db; + +import java.util.Set; + +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.Bundle; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; + +public interface DatabaseComponent { + + static final long MEGABYTES = 1024L * 1024L; + static final long GIGABYTES = 1024L * MEGABYTES; + + static final long MAX_DB_SIZE = 2L * GIGABYTES; + static final long MIN_FREE_SPACE = 300L * MEGABYTES; + static final long CRITICAL_FREE_SPACE = 100L * MEGABYTES; + static final long MAX_BYTES_BETWEEN_SPACE_CHECKS = 5L * MEGABYTES; + static final long MAX_MS_BETWEEN_SPACE_CHECKS = 60L * 1000L; // 1 min + static final long BYTES_PER_SWEEP = 5L * MEGABYTES; + static final int CLEANER_SLEEP_MS = 1000; // 1 sec + static final int RETRANSMIT_THRESHOLD = 3; + + void close() throws DbException; + + void addLocallyGeneratedMessage(Message m) throws DbException; + + void addNeighbour(NeighbourId n) throws DbException; + + void generateBundle(NeighbourId n, Bundle b) throws DbException; + + Rating getRating(AuthorId a) throws DbException; + + Set getSubscriptions() throws DbException; + + void receiveBundle(NeighbourId n, Bundle b) throws DbException; + + void setRating(AuthorId a, Rating r) throws DbException; + + void subscribe(GroupId g) throws DbException; + + void unsubscribe(GroupId g) throws DbException; +} diff --git a/api/net/sf/briar/api/db/DatabasePassword.java b/api/net/sf/briar/api/db/DatabasePassword.java new file mode 100644 index 000000000..06f5e4fb2 --- /dev/null +++ b/api/net/sf/briar/api/db/DatabasePassword.java @@ -0,0 +1,14 @@ +package net.sf.briar.api.db; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.google.inject.BindingAnnotation; + +@BindingAnnotation +@Target({ PARAMETER }) +@Retention(RUNTIME) +public @interface DatabasePassword {} diff --git a/api/net/sf/briar/api/db/DbException.java b/api/net/sf/briar/api/db/DbException.java new file mode 100644 index 000000000..1061f76e4 --- /dev/null +++ b/api/net/sf/briar/api/db/DbException.java @@ -0,0 +1,10 @@ +package net.sf.briar.api.db; + +public class DbException extends Exception { + + private static final long serialVersionUID = 3706581789209939441L; + + public DbException(Throwable t) { + super(t); + } +} diff --git a/api/net/sf/briar/api/db/NeighbourId.java b/api/net/sf/briar/api/db/NeighbourId.java new file mode 100644 index 000000000..b35a92680 --- /dev/null +++ b/api/net/sf/briar/api/db/NeighbourId.java @@ -0,0 +1,30 @@ +package net.sf.briar.api.db; + +public class NeighbourId { + + private final int id; + + public NeighbourId(int id) { + this.id = id; + } + + public int getInt() { + return id; + } + + @Override + public boolean equals(Object o) { + if(o instanceof NeighbourId) return id == ((NeighbourId) o).id; + return false; + } + + @Override + public int hashCode() { + return id; + } + + @Override + public String toString() { + return String.valueOf(id); + } +} diff --git a/api/net/sf/briar/api/db/Rating.java b/api/net/sf/briar/api/db/Rating.java new file mode 100644 index 000000000..b8b4a07f8 --- /dev/null +++ b/api/net/sf/briar/api/db/Rating.java @@ -0,0 +1,5 @@ +package net.sf.briar.api.db; + +public enum Rating { + BAD, UNRATED, GOOD +} diff --git a/api/net/sf/briar/api/db/Status.java b/api/net/sf/briar/api/db/Status.java new file mode 100644 index 000000000..ed5bba15f --- /dev/null +++ b/api/net/sf/briar/api/db/Status.java @@ -0,0 +1,5 @@ +package net.sf.briar.api.db; + +public enum Status { + NEW, SENT, SEEN +} diff --git a/api/net/sf/briar/api/i18n/FontManager.java b/api/net/sf/briar/api/i18n/FontManager.java new file mode 100644 index 000000000..87053fa5b --- /dev/null +++ b/api/net/sf/briar/api/i18n/FontManager.java @@ -0,0 +1,18 @@ +package net.sf.briar.api.i18n; + +import java.awt.Font; +import java.io.IOException; +import java.util.Locale; + +public interface FontManager { + + void initialize(Locale locale) throws IOException; + + String[] getBundledFontFilenames(); + + Font getFontForLanguage(String language); + + Font getUiFont(); + + void setUiFontForLanguage(String language); +} \ No newline at end of file diff --git a/api/net/sf/briar/api/i18n/I18n.java b/api/net/sf/briar/api/i18n/I18n.java new file mode 100644 index 000000000..2e93804ef --- /dev/null +++ b/api/net/sf/briar/api/i18n/I18n.java @@ -0,0 +1,33 @@ +package net.sf.briar.api.i18n; + +import java.awt.ComponentOrientation; +import java.awt.Font; +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +public interface I18n { + + String tr(String name); + + Locale getLocale(); + + void setLocale(Locale locale); + + void loadLocale() throws IOException; + + void saveLocale() throws IOException; + + void saveLocale(File dir) throws IOException; + + ComponentOrientation getComponentOrientation(); + + void addListener(Listener l); + + void removeListener(Listener l); + + public interface Listener { + + void localeChanged(Font uiFont); + } +} \ No newline at end of file diff --git a/api/net/sf/briar/api/i18n/Stri18ng.java b/api/net/sf/briar/api/i18n/Stri18ng.java new file mode 100644 index 000000000..250752b0e --- /dev/null +++ b/api/net/sf/briar/api/i18n/Stri18ng.java @@ -0,0 +1,41 @@ +package net.sf.briar.api.i18n; + +public class Stri18ng { + + private static final String HTML_OPEN_LEFT = ""; + private static final String HTML_OPEN_RIGHT = ""; + private static final String HTML_CLOSE = ""; + private static final String PARAGRAPH = "

"; // Yes, two of them + + private final String name; + private final I18n i18n; + + public Stri18ng(String name, I18n i18n) { + this.name = name; + this.i18n = i18n; + } + + public String tr() { + return i18n.tr(name); + } + + public String html() { + if(i18n.getComponentOrientation().isLeftToRight()) + return HTML_OPEN_LEFT + i18n.tr(name) + HTML_CLOSE; + else return HTML_OPEN_RIGHT + i18n.tr(name) + HTML_CLOSE; + } + + public String html(String... paras) { + StringBuilder s = new StringBuilder(); + if(i18n.getComponentOrientation().isLeftToRight()) + s.append(HTML_OPEN_LEFT); + else s.append(HTML_OPEN_RIGHT); + s.append(tr()); + for(String para : paras) { + s.append(PARAGRAPH); + s.append(para); + } + s.append(HTML_CLOSE); + return s.toString(); + } +} diff --git a/api/net/sf/briar/api/invitation/InvitationCallback.java b/api/net/sf/briar/api/invitation/InvitationCallback.java new file mode 100644 index 000000000..01d9fc3a6 --- /dev/null +++ b/api/net/sf/briar/api/invitation/InvitationCallback.java @@ -0,0 +1,23 @@ +package net.sf.briar.api.invitation; + +import java.io.File; +import java.util.List; + +public interface InvitationCallback { + + boolean isCancelled(); + + void copyingFile(File f); + + void encryptingFile(File f); + + void created(List files); + + void error(String message); + + void notFound(File f); + + void notDirectory(File f); + + void notAllowed(File f); +} diff --git a/api/net/sf/briar/api/invitation/InvitationParameters.java b/api/net/sf/briar/api/invitation/InvitationParameters.java new file mode 100644 index 000000000..550e78fda --- /dev/null +++ b/api/net/sf/briar/api/invitation/InvitationParameters.java @@ -0,0 +1,16 @@ +package net.sf.briar.api.invitation; + +import java.io.File; + +public interface InvitationParameters { + + boolean shouldCreateExe(); + + boolean shouldCreateJar(); + + char[] getPassword(); + + File getChosenLocation(); + + String[] getBundledFontFilenames(); +} diff --git a/api/net/sf/briar/api/invitation/InvitationWorkerFactory.java b/api/net/sf/briar/api/invitation/InvitationWorkerFactory.java new file mode 100644 index 000000000..a178f1ec6 --- /dev/null +++ b/api/net/sf/briar/api/invitation/InvitationWorkerFactory.java @@ -0,0 +1,7 @@ +package net.sf.briar.api.invitation; + +public interface InvitationWorkerFactory { + + Runnable createWorker(InvitationCallback callback, + InvitationParameters parameters); +} diff --git a/api/net/sf/briar/api/protocol/AuthorId.java b/api/net/sf/briar/api/protocol/AuthorId.java new file mode 100644 index 000000000..08b9f6976 --- /dev/null +++ b/api/net/sf/briar/api/protocol/AuthorId.java @@ -0,0 +1,36 @@ +package net.sf.briar.api.protocol; + +import java.util.Arrays; + +public class AuthorId { + + public static final int LENGTH = 32; + + public static final AuthorId SELF = new AuthorId(new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 + }); + + private final byte[] id; + + public AuthorId(byte[] id) { + assert id.length == LENGTH; + this.id = id; + } + + public byte[] getBytes() { + return id; + } + + @Override + public boolean equals(Object o) { + if(o instanceof AuthorId) + return Arrays.equals(id, ((AuthorId) o).id); + return false; + } + + @Override + public int hashCode() { + return Arrays.hashCode(id); + } +} diff --git a/api/net/sf/briar/api/protocol/Batch.java b/api/net/sf/briar/api/protocol/Batch.java new file mode 100644 index 000000000..b7503e772 --- /dev/null +++ b/api/net/sf/briar/api/protocol/Batch.java @@ -0,0 +1,13 @@ +package net.sf.briar.api.protocol; + + +public interface Batch { + + public static final long CAPACITY = 1024L * 1024L; + + public void seal(); + BatchId getId(); + long getSize(); + Iterable getMessages(); + void addMessage(Message m); +} \ No newline at end of file diff --git a/api/net/sf/briar/api/protocol/BatchId.java b/api/net/sf/briar/api/protocol/BatchId.java new file mode 100644 index 000000000..1125c47af --- /dev/null +++ b/api/net/sf/briar/api/protocol/BatchId.java @@ -0,0 +1,31 @@ +package net.sf.briar.api.protocol; + +import java.util.Arrays; + +public class BatchId { + + public static final int LENGTH = 32; + + private final byte[] id; + + public BatchId(byte[] id) { + assert id.length == LENGTH; + this.id = id; + } + + public byte[] getBytes() { + return id; + } + + @Override + public boolean equals(Object o) { + if(o instanceof BatchId) + return Arrays.equals(id, ((BatchId) o).id); + return false; + } + + @Override + public int hashCode() { + return Arrays.hashCode(id); + } +} diff --git a/api/net/sf/briar/api/protocol/Bundle.java b/api/net/sf/briar/api/protocol/Bundle.java new file mode 100644 index 000000000..5bb55e455 --- /dev/null +++ b/api/net/sf/briar/api/protocol/Bundle.java @@ -0,0 +1,16 @@ +package net.sf.briar.api.protocol; + + +public interface Bundle { + + public void seal(); + BundleId getId(); + long getCapacity(); + long getSize(); + Iterable getAcks(); + void addAck(BatchId b); + Iterable getSubscriptions(); + void addSubscription(GroupId g); + Iterable getBatches(); + void addBatch(Batch b); +} diff --git a/api/net/sf/briar/api/protocol/BundleId.java b/api/net/sf/briar/api/protocol/BundleId.java new file mode 100644 index 000000000..9dba86d20 --- /dev/null +++ b/api/net/sf/briar/api/protocol/BundleId.java @@ -0,0 +1,36 @@ +package net.sf.briar.api.protocol; + +import java.util.Arrays; + +public class BundleId { + + public static final BundleId NONE = new BundleId(new byte[] { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }); + + public static final int LENGTH = 32; + + private final byte[] id; + + public BundleId(byte[] id) { + assert id.length == LENGTH; + this.id = id; + } + + public byte[] getBytes() { + return id; + } + + @Override + public boolean equals(Object o) { + if(o instanceof BundleId) + return Arrays.equals(id, ((BundleId) o).id); + return false; + } + + @Override + public int hashCode() { + return Arrays.hashCode(id); + } +} diff --git a/api/net/sf/briar/api/protocol/GroupId.java b/api/net/sf/briar/api/protocol/GroupId.java new file mode 100644 index 000000000..1ac038bbf --- /dev/null +++ b/api/net/sf/briar/api/protocol/GroupId.java @@ -0,0 +1,31 @@ +package net.sf.briar.api.protocol; + +import java.util.Arrays; + +public class GroupId { + + public static final int LENGTH = 32; + + private final byte[] id; + + public GroupId(byte[] id) { + assert id.length == LENGTH; + this.id = id; + } + + public byte[] getBytes() { + return id; + } + + @Override + public boolean equals(Object o) { + if(o instanceof GroupId) + return Arrays.equals(id, ((GroupId) o).id); + return false; + } + + @Override + public int hashCode() { + return Arrays.hashCode(id); + } +} diff --git a/api/net/sf/briar/api/protocol/Message.java b/api/net/sf/briar/api/protocol/Message.java new file mode 100644 index 000000000..31458e492 --- /dev/null +++ b/api/net/sf/briar/api/protocol/Message.java @@ -0,0 +1,13 @@ +package net.sf.briar.api.protocol; + + +public interface Message { + + MessageId getId(); + MessageId getParent(); + GroupId getGroup(); + AuthorId getAuthor(); + long getTimestamp(); + int getSize(); + byte[] getBody(); +} \ No newline at end of file diff --git a/api/net/sf/briar/api/protocol/MessageFactory.java b/api/net/sf/briar/api/protocol/MessageFactory.java new file mode 100644 index 000000000..33f0fdc43 --- /dev/null +++ b/api/net/sf/briar/api/protocol/MessageFactory.java @@ -0,0 +1,7 @@ +package net.sf.briar.api.protocol; + +public interface MessageFactory { + + Message createMessage(MessageId id, MessageId parent, GroupId group, + AuthorId author, long timestamp, byte[] body); +} diff --git a/api/net/sf/briar/api/protocol/MessageId.java b/api/net/sf/briar/api/protocol/MessageId.java new file mode 100644 index 000000000..7fb042a29 --- /dev/null +++ b/api/net/sf/briar/api/protocol/MessageId.java @@ -0,0 +1,36 @@ +package net.sf.briar.api.protocol; + +import java.util.Arrays; + +public class MessageId { + + public static final MessageId NONE = new MessageId(new byte[] { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }); + + public static final int LENGTH = 32; + + private final byte[] id; + + public MessageId(byte[] id) { + assert id.length == LENGTH; + this.id = id; + } + + public byte[] getBytes() { + return id; + } + + @Override + public boolean equals(Object o) { + if(o instanceof MessageId) + return Arrays.equals(id, ((MessageId) o).id); + return false; + } + + @Override + public int hashCode() { + return Arrays.hashCode(id); + } +} diff --git a/api/net/sf/briar/api/setup/SetupCallback.java b/api/net/sf/briar/api/setup/SetupCallback.java new file mode 100644 index 000000000..1c3767c00 --- /dev/null +++ b/api/net/sf/briar/api/setup/SetupCallback.java @@ -0,0 +1,22 @@ +package net.sf.briar.api.setup; + +import java.io.File; + +public interface SetupCallback { + + boolean isCancelled(); + + void extractingFile(File f); + + void copyingFile(File f); + + void installed(File f); + + void error(String message); + + void notFound(File f); + + void notDirectory(File f); + + void notAllowed(File f); +} diff --git a/api/net/sf/briar/api/setup/SetupParameters.java b/api/net/sf/briar/api/setup/SetupParameters.java new file mode 100644 index 000000000..9408e9b82 --- /dev/null +++ b/api/net/sf/briar/api/setup/SetupParameters.java @@ -0,0 +1,10 @@ +package net.sf.briar.api.setup; + +import java.io.File; + +public interface SetupParameters { + + File getChosenLocation(); + + String[] getBundledFontFilenames(); +} diff --git a/api/net/sf/briar/api/setup/SetupWorkerFactory.java b/api/net/sf/briar/api/setup/SetupWorkerFactory.java new file mode 100644 index 000000000..65b60580c --- /dev/null +++ b/api/net/sf/briar/api/setup/SetupWorkerFactory.java @@ -0,0 +1,6 @@ +package net.sf.briar.api.setup; + +public interface SetupWorkerFactory { + + Runnable createWorker(SetupCallback callback, SetupParameters parameters); +} diff --git a/build-common.xml b/build-common.xml new file mode 100644 index 000000000..dd1a59c5a --- /dev/null +++ b/build-common.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.xml b/build.xml new file mode 100644 index 000000000..e1daec61b --- /dev/null +++ b/build.xml @@ -0,0 +1,3 @@ + + + diff --git a/components/.gitignore b/components/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/components/.gitignore @@ -0,0 +1 @@ +/build diff --git a/components/build.xml b/components/build.xml new file mode 100644 index 000000000..88fdea5bf --- /dev/null +++ b/components/build.xml @@ -0,0 +1,3 @@ + + + diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java new file mode 100644 index 000000000..29b048fd8 --- /dev/null +++ b/components/net/sf/briar/db/Database.java @@ -0,0 +1,114 @@ +package net.sf.briar.db; + +import java.util.Set; + +import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.NeighbourId; +import net.sf.briar.api.db.Rating; +import net.sf.briar.api.db.Status; +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.BundleId; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; + +interface Database { + + void open(boolean resume) throws DbException; + + void close() throws DbException; + + T startTransaction(String name) throws DbException; + + void abortTransaction(T txn); + + void commitTransaction(T txn) throws DbException; + + // Locking: neighbours write + void addBatchToAck(T txn, NeighbourId n, BatchId b) throws DbException; + + // Locking: neighbours write + void addNeighbour(T txn, NeighbourId n) throws DbException; + + // Locking: neighbours write, messages read + void addOutstandingBatch(T txn, NeighbourId n, BatchId b, Set sent) throws DbException; + + // Locking: neighbours write, messages read + Set addReceivedBundle(T txn, NeighbourId n, BundleId b) throws DbException; + + // Locking: subscriptions write + void addSubscription(T txn, GroupId g) throws DbException; + + // Locking: neighbours write + void addSubscription(T txn, NeighbourId n, GroupId g) throws DbException; + + // Locking: neighbours write + void clearSubscriptions(T txn, NeighbourId n) throws DbException; + + // Locking: messages read + boolean containsMessage(T txn, MessageId m) throws DbException; + + // Locking: subscriptions read + boolean containsSubscription(T txn, GroupId g) throws DbException; + + // Locking: messages read + long getFreeSpace() throws DbException; + + // Locking: messages read + Message getMessage(T txn, MessageId m) throws DbException; + + // Locking: messages read + Iterable getMessagesByAuthor(T txn, AuthorId a) throws DbException; + + // Locking: messages read + Iterable getMessagesByParent(T txn, MessageId m) throws DbException; + + // Locking: neighbours read + Set getNeighbours(T txn) throws DbException; + + // Locking: messages read + Iterable getOldMessages(T txn, long size) throws DbException; + + // Locking: messages read + MessageId getParent(T txn, MessageId m) throws DbException; + + // Locking: ratings read + Rating getRating(T txn, AuthorId a) throws DbException; + + // Locking: messages read + int getSendability(T txn, MessageId m) throws DbException; + + // Locking: neighbours read, messages read + Iterable getSendableMessages(T txn, NeighbourId n, long capacity) throws DbException; + + // Locking: subscriptions read + Set getSubscriptions(T txn) throws DbException; + + // Locking: messages write + boolean addMessage(T txn, Message m) throws DbException; + + // Locking: ratings write + Rating setRating(T txn, AuthorId a, Rating r) throws DbException; + + // Locking: messages write + void setSendability(T txn, MessageId m, int sendability) throws DbException; + + // Locking: neighbours read, n write + Set removeBatchesToAck(T txn, NeighbourId n) throws DbException; + + // Locking: neighbours write, messages read + void removeLostBatch(T txn, NeighbourId n, BatchId b) throws DbException; + + // Locking: neighbours write, messages write + void removeMessage(T txn, MessageId m) throws DbException; + + // Locking: neighbours write + Set removeOutstandingBatch(T txn, NeighbourId n, BatchId b) throws DbException; + + // Locking: subscriptions write, neighbours write, messages write + void removeSubscription(T txn, GroupId g) throws DbException; + + // Locking: neighbours write, messages read + void setStatus(T txn, NeighbourId n, MessageId m, Status s) throws DbException; +} diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java new file mode 100644 index 000000000..e0f447cdb --- /dev/null +++ b/components/net/sf/briar/db/DatabaseComponentImpl.java @@ -0,0 +1,209 @@ +package net.sf.briar.db; + +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.NeighbourId; +import net.sf.briar.api.db.Rating; +import net.sf.briar.api.db.Status; +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; + +import com.google.inject.Provider; + +abstract class DatabaseComponentImpl implements DatabaseComponent { + + protected final Database db; + protected final Provider batchProvider; + + private final Object spaceLock = new Object(); + private final Object writeLock = new Object(); + private long bytesStoredSinceLastCheck = 0L; // Locking: spaceLock + private long timeOfLastCheck = 0L; // Locking: spaceLock + private volatile boolean writesAllowed = true; + + DatabaseComponentImpl(Database db, Provider batchProvider) { + this.db = db; + this.batchProvider = batchProvider; + startCleaner(); + } + + protected abstract void expireMessages(long size) throws DbException; + + // Locking: messages write + private int calculateSendability(Txn txn, Message m) throws DbException { + int sendability = 0; + // One point for a good rating + if(getRating(m.getAuthor()) == Rating.GOOD) sendability++; + // One point per sendable child (backward inclusion) + for(MessageId kid : db.getMessagesByParent(txn, m.getId())) { + Integer kidSendability = db.getSendability(txn, kid); + assert kidSendability != null; + if(kidSendability > 0) sendability++; + } + return sendability; + } + + private void checkFreeSpaceAndClean() throws DbException { + long freeSpace = db.getFreeSpace(); + while(freeSpace < MIN_FREE_SPACE) { + // If disk space is critical, disable the storage of new messages + if(freeSpace < CRITICAL_FREE_SPACE) { + System.out.println("Critical cleanup"); + writesAllowed = false; + } else { + System.out.println("Normal cleanup"); + } + expireMessages(BYTES_PER_SWEEP); + Thread.yield(); + freeSpace = db.getFreeSpace(); + // If disk space is no longer critical, re-enable writes + if(freeSpace >= CRITICAL_FREE_SPACE && !writesAllowed) { + writesAllowed = true; + synchronized(writeLock) { + writeLock.notifyAll(); + } + } + } + } + + // Locking: messages write, neighbours write + protected void removeMessage(Txn txn, MessageId id) throws DbException { + Integer sendability = db.getSendability(txn, id); + assert sendability != null; + if(sendability > 0) updateAncestorSendability(txn, id, false); + db.removeMessage(txn, id); + } + + private boolean shouldCheckFreeSpace() { + synchronized(spaceLock) { + long now = System.currentTimeMillis(); + if(bytesStoredSinceLastCheck > MAX_BYTES_BETWEEN_SPACE_CHECKS) { + System.out.println(bytesStoredSinceLastCheck + + " bytes stored since last check"); + bytesStoredSinceLastCheck = 0L; + timeOfLastCheck = now; + return true; + } + if(now - timeOfLastCheck > MAX_MS_BETWEEN_SPACE_CHECKS) { + System.out.println((now - timeOfLastCheck) + + " ms since last check"); + bytesStoredSinceLastCheck = 0L; + timeOfLastCheck = now; + return true; + } + } + return false; + } + + private void startCleaner() { + Runnable cleaner = new Runnable() { + public void run() { + try { + while(true) { + if(shouldCheckFreeSpace()) { + checkFreeSpaceAndClean(); + } else { + try { + Thread.sleep(CLEANER_SLEEP_MS); + } catch(InterruptedException ignored) {} + } + } + } catch(Throwable t) { + // FIXME: Work out what to do here + t.printStackTrace(); + System.exit(1); + } + } + }; + new Thread(cleaner).start(); + } + + // Locking: messages write, neighbours write + protected boolean storeMessage(Txn txn, Message m, NeighbourId sender) + throws DbException { + boolean added = db.addMessage(txn, m); + // Mark the message as seen by the sender + MessageId id = m.getId(); + if(sender != null) db.setStatus(txn, sender, id, Status.SEEN); + if(added) { + // Mark the message as unseen by other neighbours + for(NeighbourId n : db.getNeighbours(txn)) { + if(!n.equals(sender)) db.setStatus(txn, n, id, Status.NEW); + } + // Calculate and store the message's sendability + int sendability = calculateSendability(txn, m); + db.setSendability(txn, id, sendability); + if(sendability > 0) updateAncestorSendability(txn, id, true); + // Count the bytes stored + synchronized(spaceLock) { + bytesStoredSinceLastCheck += m.getSize(); + } + } + return added; + } + + // Locking: messages write + private int updateAncestorSendability(Txn txn, MessageId m, + boolean increment) throws DbException { + int affected = 0; + boolean changed = true; + while(changed) { + MessageId parent = db.getParent(txn, m); + if(parent.equals(MessageId.NONE)) break; + if(!db.containsMessage(txn, parent)) break; + Integer parentSendability = db.getSendability(txn, parent); + assert parentSendability != null; + if(increment) { + parentSendability++; + changed = parentSendability == 1; + if(changed) affected++; + } else { + assert parentSendability > 0; + parentSendability--; + changed = parentSendability == 0; + if(changed) affected++; + } + db.setSendability(txn, parent, parentSendability); + m = parent; + } + return affected; + } + + // Locking: messages write + protected void updateAuthorSendability(Txn txn, AuthorId a, + boolean increment) throws DbException { + int direct = 0, indirect = 0; + for(MessageId id : db.getMessagesByAuthor(txn, a)) { + int sendability = db.getSendability(txn, id); + if(increment) { + db.setSendability(txn, id, sendability + 1); + if(sendability == 0) { + direct++; + indirect += updateAncestorSendability(txn, id, true); + } + } else { + assert sendability > 0; + db.setSendability(txn, id, sendability - 1); + if(sendability == 1) { + direct++; + indirect += updateAncestorSendability(txn, id, false); + } + } + } + System.out.println(direct + " messages affected directly, " + + indirect + " indirectly"); + } + + protected void waitForPermissionToWrite() { + synchronized(writeLock) { + while(!writesAllowed) { + System.out.println("Waiting for permission to write"); + try { + writeLock.wait(); + } catch(InterruptedException ignored) {} + } + } + } +} diff --git a/components/net/sf/briar/db/DatabaseModule.java b/components/net/sf/briar/db/DatabaseModule.java new file mode 100644 index 000000000..1150323be --- /dev/null +++ b/components/net/sf/briar/db/DatabaseModule.java @@ -0,0 +1,22 @@ +package net.sf.briar.db; + +import net.sf.briar.api.crypto.Password; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.db.DatabasePassword; + +import com.google.inject.AbstractModule; +import com.google.inject.Singleton; + +public class DatabaseModule extends AbstractModule { + + @Override + protected void configure() { + bind(Database.class).to(H2Database.class); + bind(DatabaseComponent.class).to(ReadWriteLockDatabaseComponent.class).in(Singleton.class); + bind(Password.class).annotatedWith(DatabasePassword.class).toInstance(new Password() { + public char[] getPassword() { + return "fixme fixme".toCharArray(); + } + }); + } +} diff --git a/components/net/sf/briar/db/H2Database.java b/components/net/sf/briar/db/H2Database.java new file mode 100644 index 000000000..4714b4ba0 --- /dev/null +++ b/components/net/sf/briar/db/H2Database.java @@ -0,0 +1,70 @@ +package net.sf.briar.db; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Properties; + +import net.sf.briar.api.crypto.Password; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.db.DatabasePassword; +import net.sf.briar.api.db.DbException; +import net.sf.briar.api.protocol.MessageFactory; +import net.sf.briar.util.FileUtils; + +import com.google.inject.Inject; + +class H2Database extends JdbcDatabase { + + private final Password password; + private final File home; + private final String url; + + @Inject + H2Database(MessageFactory messageFactory, + @DatabasePassword Password password) { + super(messageFactory, "BINARY(32)"); + this.password = password; + home = new File(FileUtils.getBriarDirectory(), "Data/db/db"); + url = "jdbc:h2:split:" + home.getPath() + + ";CIPHER=AES;DB_CLOSE_ON_EXIT=false"; + } + + public void open(boolean resume) throws DbException { + super.open(resume, home.getParentFile(), "org.h2.Driver"); + } + + public void close() throws DbException { + System.out.println("Closing database"); + try { + super.closeAllConnections(); + } catch(SQLException e) { + throw new DbException(e); + } + } + + public long getFreeSpace() throws DbException { + File dir = home.getParentFile(); + long free = dir.getFreeSpace(); + long used = getDiskSpace(dir); + long quota = DatabaseComponent.MAX_DB_SIZE - used; + long min = Math.min(free, quota); + System.out.println("Free space: " + min); + return min; + } + + @Override + protected Connection createConnection() throws SQLException { + Properties props = new Properties(); + props.setProperty("user", "b"); + char[] passwordArray = password.getPassword(); + props.put("password", passwordArray); + try { + return DriverManager.getConnection(url, props); + } finally { + Arrays.fill(passwordArray, (char) 0); + } + } +} diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java new file mode 100644 index 000000000..79ac79f52 --- /dev/null +++ b/components/net/sf/briar/db/JdbcDatabase.java @@ -0,0 +1,1204 @@ +package net.sf.briar.db; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.sql.Blob; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.NeighbourId; +import net.sf.briar.api.db.Rating; +import net.sf.briar.api.db.Status; +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.BundleId; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageFactory; +import net.sf.briar.api.protocol.MessageId; + +abstract class JdbcDatabase implements Database { + + private static final String CREATE_LOCAL_SUBSCRIPTIONS = + "CREATE TABLE localSubscriptions" + + " (groupId XXXX NOT NULL," + + " PRIMARY KEY (groupId))"; + + private static final String CREATE_MESSAGES = + "CREATE TABLE messages" + + " (messageId XXXX NOT NULL," + + " parentId XXXX NOT NULL," + + " groupId XXXX NOT NULL," + + " authorId XXXX NOT NULL," + + " timestamp BIGINT NOT NULL," + + " size INT NOT NULL," + + " body BLOB NOT NULL," + + " sendability INT NOT NULL," + + " PRIMARY KEY (messageId)," + + " FOREIGN KEY (groupId) REFERENCES localSubscriptions (groupId)" + + " ON DELETE CASCADE)"; + + private static final String INDEX_MESSAGES_BY_PARENT = + "CREATE INDEX messagesByParent ON messages (parentId)"; + + private static final String INDEX_MESSAGES_BY_AUTHOR = + "CREATE INDEX messagesByAuthor ON messages (authorId)"; + + private static final String INDEX_MESSAGES_BY_TIMESTAMP = + "CREATE INDEX messagesByTimestamp ON messages (timestamp)"; + + private static final String INDEX_MESSAGES_BY_SENDABILITY = + "CREATE INDEX messagesBySendability ON messages (sendability)"; + + private static final String CREATE_NEIGHBOURS = + "CREATE TABLE neighbours" + + " (neighbourId INT NOT NULL," + + " lastBundleReceived XXXX NOT NULL," + + " PRIMARY KEY (neighbourId))"; + + private static final String CREATE_BATCHES_TO_ACK = + "CREATE TABLE batchesToAck" + + " (batchId XXXX NOT NULL," + + " neighbourId INT NOT NULL," + + " PRIMARY KEY (batchId)," + + " FOREIGN KEY (neighbourId) REFERENCES neighbours (neighbourId)" + + " ON DELETE CASCADE)"; + + private static final String CREATE_NEIGHBOUR_SUBSCRIPTIONS = + "CREATE TABLE neighbourSubscriptions" + + " (neighbourId INT NOT NULL," + + " groupId XXXX NOT NULL," + + " PRIMARY KEY (neighbourId, groupId)," + + " FOREIGN KEY (neighbourId) REFERENCES neighbours (neighbourId)" + + " ON DELETE CASCADE)"; + + private static final String CREATE_OUTSTANDING_BATCHES = + "CREATE TABLE outstandingBatches" + + " (batchId XXXX NOT NULL," + + " neighbourId INT NOT NULL," + + " lastBundleReceived XXXX NOT NULL," + + " PRIMARY KEY (batchId)," + + " FOREIGN KEY (neighbourId) REFERENCES neighbours (neighbourId)" + + " ON DELETE CASCADE)"; + + private static final String CREATE_OUTSTANDING_MESSAGES = + "CREATE TABLE outstandingMessages" + + " (batchId XXXX NOT NULL," + + " neighbourId INT NOT NULL," + + " messageId XXXX NOT NULL," + + " PRIMARY KEY (batchId, messageId)," + + " FOREIGN KEY (batchId) REFERENCES outstandingBatches (batchId)" + + " ON DELETE CASCADE," + + " FOREIGN KEY (neighbourId) REFERENCES neighbours (neighbourId)" + + " ON DELETE CASCADE," + + " FOREIGN KEY (messageId) REFERENCES messages (messageId)" + + " ON DELETE CASCADE)"; + + private static final String INDEX_OUTSTANDING_MESSAGES_BY_BATCH = + "CREATE INDEX outstandingMessagesByBatch" + + " ON outstandingMessages (batchId)"; + + private static final String CREATE_RATINGS = + "CREATE TABLE ratings" + + " (authorId XXXX NOT NULL," + + " rating SMALLINT NOT NULL," + + " PRIMARY KEY (authorId))"; + + private static final String CREATE_RECEIVED_BUNDLES = + "CREATE TABLE receivedBundles" + + " (bundleId XXXX NOT NULL," + + " neighbourId INT NOT NULL," + + " timestamp BIGINT NOT NULL," + + " PRIMARY KEY (bundleId)," + + " FOREIGN KEY (neighbourId) REFERENCES neighbours (neighbourId)" + + " ON DELETE CASCADE)"; + + private static final String CREATE_STATUSES = + "CREATE TABLE statuses" + + " (messageId XXXX NOT NULL," + + " neighbourId INT NOT NULL," + + " status SMALLINT NOT NULL," + + " PRIMARY KEY (messageId, neighbourId)," + + " FOREIGN KEY (messageId) REFERENCES messages (messageId)" + + " ON DELETE CASCADE," + + " FOREIGN KEY (neighbourId) REFERENCES neighbours (neighbourId)" + + " ON DELETE CASCADE)"; + + private static final String INDEX_STATUSES_BY_MESSAGE = + "CREATE INDEX statusesByMessage ON statuses (messageId)"; + + private static final String INDEX_STATUSES_BY_NEIGHBOUR = + "CREATE INDEX statusesByNeighbour ON statuses (neighbourId)"; + + private final MessageFactory messageFactory; + private final String hashType; + private final LinkedList connections = + new LinkedList(); // Locking: self + + private volatile int openConnections = 0; // Locking: connections + private volatile boolean closed = false; // Locking: connections + + protected abstract Connection createConnection() throws SQLException; + + JdbcDatabase(MessageFactory messageFactory, String hashType) { + this.messageFactory = messageFactory; + this.hashType = hashType; + } + + protected void open(boolean resume, File dir, String driverClass) + throws DbException { + if(resume) { + assert dir.exists(); + assert dir.isDirectory(); + System.out.println("Resuming from " + dir.getPath()); + } else { + if(dir.exists()) delete(dir); + } + try { + Class.forName(driverClass); + } catch(ClassNotFoundException e) { + throw new DbException(e); + } + Connection txn = startTransaction("initialize"); + try { + // If not resuming, create the tables + if(resume) + System.out.println(getNumberOfMessages(txn) + " messages"); + else createTables(txn); + commitTransaction(txn); + } catch(DbException e) { + abortTransaction(txn); + throw e; + } + } + + private void delete(File f) { + if(f.isDirectory()) for(File child : f.listFiles()) delete(child); + System.out.println("Deleting " + f.getPath()); + f.delete(); + } + + private void createTables(Connection txn) throws DbException { + Statement s = null; + try { + s = txn.createStatement(); + System.out.println("Creating localSubscriptions table"); + s.executeUpdate(insertHashType(CREATE_LOCAL_SUBSCRIPTIONS)); + System.out.println("Creating messages table"); + s.executeUpdate(insertHashType(CREATE_MESSAGES)); + s.executeUpdate(INDEX_MESSAGES_BY_PARENT); + s.executeUpdate(INDEX_MESSAGES_BY_AUTHOR); + s.executeUpdate(INDEX_MESSAGES_BY_TIMESTAMP); + s.executeUpdate(INDEX_MESSAGES_BY_SENDABILITY); + System.out.println("Creating neighbours table"); + s.executeUpdate(insertHashType(CREATE_NEIGHBOURS)); + System.out.println("Creating batchesToAck table"); + s.executeUpdate(insertHashType(CREATE_BATCHES_TO_ACK)); + System.out.println("Creating neighbourSubscriptions table"); + s.executeUpdate(insertHashType(CREATE_NEIGHBOUR_SUBSCRIPTIONS)); + System.out.println("Creating outstandingBatches table"); + s.executeUpdate(insertHashType(CREATE_OUTSTANDING_BATCHES)); + System.out.println("Creating outstandingMessages table"); + s.executeUpdate(insertHashType(CREATE_OUTSTANDING_MESSAGES)); + s.executeUpdate(INDEX_OUTSTANDING_MESSAGES_BY_BATCH); + System.out.println("Creating ratings table"); + s.executeUpdate(insertHashType(CREATE_RATINGS)); + System.out.println("Creating receivedBundles table"); + s.executeUpdate(insertHashType(CREATE_RECEIVED_BUNDLES)); + System.out.println("Creating statuses table"); + s.executeUpdate(insertHashType(CREATE_STATUSES)); + s.executeUpdate(INDEX_STATUSES_BY_MESSAGE); + s.executeUpdate(INDEX_STATUSES_BY_NEIGHBOUR); + s.close(); + } catch(SQLException e) { + tryToClose(s); + tryToClose(txn); + throw new DbException(e); + } + } + + // FIXME: Get rid of this if we're definitely not using Derby + private String insertHashType(String s) { + return s.replaceAll("XXXX", hashType); + } + + private void tryToClose(Connection c) { + if(c != null) try { + c.close(); + } catch(SQLException ignored) {} + } + + private void tryToClose(Statement s) { + if(s != null) try { + s.close(); + } catch(SQLException ignored) {} + } + + private void tryToClose(ResultSet rs) { + if(rs != null) try { + rs.close(); + } catch(SQLException ignored) {} + } + + public Connection startTransaction(String name) throws DbException { + Connection txn = null; + try { + synchronized(connections) { + // If the database has been closed, don't return + while(closed) { + try { + connections.wait(); + } catch(InterruptedException ignored) {} + } + txn = connections.poll(); + } + if(txn == null) { + txn = createConnection(); + assert txn != null; + synchronized(connections) { + openConnections++; + System.out.println(openConnections + " open connections"); + } + } + txn.setAutoCommit(false); + return txn; + } catch(SQLException e) { + tryToClose(txn); + throw new DbException(e); + } + } + + public void abortTransaction(Connection txn) { + try { + txn.rollback(); + txn.setAutoCommit(true); + synchronized(connections) { + connections.add(txn); + connections.notifyAll(); + } + } catch(SQLException e) { + tryToClose(txn); + } + } + + public void commitTransaction(Connection txn) throws DbException { + try { + txn.commit(); + txn.setAutoCommit(true); + synchronized(connections) { + connections.add(txn); + connections.notifyAll(); + } + } catch(SQLException e) { + tryToClose(txn); + throw new DbException(e); + } + } + + protected void closeAllConnections() throws SQLException { + synchronized(connections) { + closed = true; + for(Connection c : connections) c.close(); + openConnections -= connections.size(); + connections.clear(); + while(openConnections > 0) { + System.out.println("Waiting for " + openConnections + + " open connections"); + try { + connections.wait(); + } catch(InterruptedException ignored) {} + for(Connection c : connections) c.close(); + openConnections -= connections.size(); + connections.clear(); + } + } + } + + public void addBatchToAck(Connection txn, NeighbourId n, BatchId b) + throws DbException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO batchesToAck" + + " (batchId, neighbourId)" + + " VALUES (?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, b.getBytes()); + ps.setInt(2, n.getInt()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public boolean addMessage(Connection txn, Message m) throws DbException { + if(containsMessage(txn, m.getId())) return false; + PreparedStatement ps = null; + try { + String sql = "INSERT INTO messages" + + " (messageId, parentId, groupId, authorId, timestamp, size," + + " body, sendability)" + + " VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getId().getBytes()); + ps.setBytes(2, m.getParent().getBytes()); + ps.setBytes(3, m.getGroup().getBytes()); + ps.setBytes(4, m.getAuthor().getBytes()); + ps.setLong(5, m.getTimestamp()); + ps.setInt(6, m.getSize()); + ps.setBlob(7, new ByteArrayInputStream(m.getBody())); + ps.setInt(8, 0); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + return true; + } catch(SQLException e) { + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public void addNeighbour(Connection txn, NeighbourId n) throws DbException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO neighbours" + + " (neighbourId, lastBundleReceived)" + + " VALUES (?, ?)"; + ps = txn.prepareStatement(sql); + ps.setInt(1, n.getInt()); + ps.setBytes(2, BundleId.NONE.getBytes()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public void addOutstandingBatch(Connection txn, NeighbourId n, BatchId b, + Set sent) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + // Find the ID of the last bundle received from n + String sql = "SELECT lastBundleReceived FROM neighbours" + + " WHERE neighbourId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, n.getInt()); + rs = ps.executeQuery(); + boolean found = rs.next(); + assert found; + byte[] lastBundleReceived = rs.getBytes(1); + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + // Create an outstanding batch row + sql = "INSERT INTO outstandingBatches" + + " (batchId, neighbourId, lastBundleReceived)" + + " VALUES (?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, b.getBytes()); + ps.setInt(2, n.getInt()); + ps.setBytes(3, lastBundleReceived); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + // Create an outstanding message row for each message in the batch + sql = "INSERT INTO outstandingMessages" + + " (batchId, neighbourId, messageId)" + + " VALUES (?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, b.getBytes()); + ps.setInt(2, n.getInt()); + for(MessageId m : sent) { + ps.setBytes(3, m.getBytes()); + ps.addBatch(); + } + int[] rowsAffected1 = ps.executeBatch(); + assert rowsAffected1.length == sent.size(); + for(int i = 0; i < rowsAffected1.length; i++) { + assert rowsAffected1[i] == 1; + } + ps.close(); + // Set the status of each message in the batch to SENT + sql = "UPDATE statuses SET status = ?" + + " WHERE messageId = ? AND neighbourId = ? AND status = ?"; + ps = txn.prepareStatement(sql); + ps.setShort(1, (short) Status.SENT.ordinal()); + ps.setInt(3, n.getInt()); + ps.setShort(4, (short) Status.NEW.ordinal()); + for(MessageId m : sent) { + ps.setBytes(2, m.getBytes()); + ps.addBatch(); + } + rowsAffected1 = ps.executeBatch(); + assert rowsAffected1.length == sent.size(); + for(int i = 0; i < rowsAffected1.length; i++) { + assert rowsAffected1[i] <= 1; + } + ps.close(); + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public Set addReceivedBundle(Connection txn, NeighbourId n, + BundleId b) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + // Update the ID of the last bundle received from n + String sql = "UPDATE neighbours SET lastBundleReceived = ?" + + " WHERE neighbourId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, b.getBytes()); + ps.setInt(2, n.getInt()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + // Count the received bundle records for n and find the oldest + sql = "SELECT bundleId, timestamp FROM receivedBundles" + + " WHERE neighbourId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, n.getInt()); + rs = ps.executeQuery(); + int received = 0; + long oldestTimestamp = Long.MAX_VALUE; + byte[] oldestBundle = null; + while(rs.next()) { + received++; + byte[] bundle = rs.getBytes(1); + long timestamp = rs.getLong(2); + if(timestamp < oldestTimestamp) { + oldestTimestamp = timestamp; + oldestBundle = bundle; + } + } + rs.close(); + ps.close(); + Set lost; + if(received == DatabaseComponent.RETRANSMIT_THRESHOLD) { + // Expire batches related to the oldest received bundle + assert oldestBundle != null; + lost = findLostBatches(txn, n, oldestBundle); + sql = "DELETE FROM receivedBundles WHERE bundleId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, oldestBundle); + rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } else { + lost = Collections.emptySet(); + } + // Record the new received bundle + sql = "INSERT INTO receivedBundles" + + " (bundleId, neighbourId, timestamp)" + + " VALUES (?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, b.getBytes()); + ps.setInt(2, n.getInt()); + ps.setLong(3, System.currentTimeMillis()); + rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + return lost; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + private Set findLostBatches(Connection txn, NeighbourId n, + byte[] lastBundleReceived) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT batchId FROM outstandingBatches" + + " WHERE neighbourId = ? AND lastBundleReceived = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, n.getInt()); + ps.setBytes(2, lastBundleReceived); + rs = ps.executeQuery(); + Set lost = new HashSet(); + while(rs.next()) lost.add(new BatchId(rs.getBytes(1))); + rs.close(); + ps.close(); + return lost; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public void addSubscription(Connection txn, GroupId g) throws DbException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO localSubscriptions (groupId) VALUES (?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public void addSubscription(Connection txn, NeighbourId n, GroupId g) + throws DbException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO neighbourSubscriptions" + + " (neighbourId, groupId)" + + " VALUES (?, ?)"; + ps = txn.prepareStatement(sql); + ps.setInt(1, n.getInt()); + ps.setBytes(2, g.getBytes()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public void clearSubscriptions(Connection txn, NeighbourId n) + throws DbException { + PreparedStatement ps = null; + try { + String sql = "DELETE FROM neighbourSubscriptions" + + " WHERE neighbourId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, n.getInt()); + ps.executeUpdate(); + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public boolean containsMessage(Connection txn, MessageId m) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT COUNT(messageId) FROM messages" + + " WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + boolean found = rs.next(); + assert found; + int count = rs.getInt(1); + assert count <= 1; + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + return count > 0; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public boolean containsSubscription(Connection txn, GroupId g) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT COUNT(groupId) FROM localSubscriptions" + + " WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + boolean found = rs.next(); + assert found; + int count = rs.getInt(1); + assert count <= 1; + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + return count > 0; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + protected long getDiskSpace(File f) { + long total = 0L; + if(f.isDirectory()) { + for(File child : f.listFiles()) total += getDiskSpace(child); + return total; + } else return f.length(); + } + + public Message getMessage(Connection txn, MessageId m) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = + "SELECT parentId, groupId, authorId, timestamp, size, body" + + " FROM messages WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + boolean found = rs.next(); + assert found; + MessageId parent = new MessageId(rs.getBytes(1)); + GroupId group = new GroupId(rs.getBytes(2)); + AuthorId author = new AuthorId(rs.getBytes(3)); + long timestamp = rs.getLong(4); + int size = rs.getInt(5); + Blob b = rs.getBlob(6); + byte[] body = b.getBytes(1, size); + assert body.length == size; + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + return messageFactory.createMessage(m, parent, group, author, + timestamp, body); + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public Iterable getMessagesByAuthor(Connection txn, AuthorId a) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT messageId FROM messages WHERE authorId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, a.getBytes()); + rs = ps.executeQuery(); + List ids = new ArrayList(); + while(rs.next()) ids.add(new MessageId(rs.getBytes(1))); + rs.close(); + ps.close(); + return ids; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public Iterable getMessagesByParent(Connection txn, MessageId m) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT messageId FROM messages WHERE parentId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + List ids = new ArrayList(); + while(rs.next()) ids.add(new MessageId(rs.getBytes(1))); + rs.close(); + ps.close(); + return ids; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public Set getNeighbours(Connection txn) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT neighbourId FROM neighbours"; + ps = txn.prepareStatement(sql); + rs = ps.executeQuery(); + Set ids = new HashSet(); + while(rs.next()) ids.add(new NeighbourId(rs.getInt(1))); + rs.close(); + ps.close(); + return ids; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public int getNumberOfMessages(Connection txn) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT COUNT(messageId) FROM messages"; + ps = txn.prepareStatement(sql); + rs = ps.executeQuery(); + boolean found = rs.next(); + assert found; + int count = rs.getInt(1); + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + return count; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public Iterable getOldMessages(Connection txn, long capacity) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT size, messageId FROM messages" + + " ORDER BY timestamp"; + ps = txn.prepareStatement(sql); + rs = ps.executeQuery(); + List ids = new ArrayList(); + long total = 0L; + while(rs.next()) { + int size = rs.getInt(1); + if(total + size > capacity) break; + ids.add(new MessageId(rs.getBytes(2))); + total += size; + } + rs.close(); + ps.close(); + System.out.println(ids.size() + " old messages, " + total + + " bytes"); + return ids; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public MessageId getParent(Connection txn, MessageId m) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT parentId FROM messages WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + boolean found = rs.next(); + assert found; + byte[] parent = rs.getBytes(1); + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + return new MessageId(parent); + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public Rating getRating(Connection txn, AuthorId a) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT rating FROM ratings WHERE authorId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, a.getBytes()); + rs = ps.executeQuery(); + Rating r; + if(rs.next()) r = Rating.values()[rs.getByte(1)]; + else r = Rating.UNRATED; + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + return r; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public int getSendability(Connection txn, MessageId m) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT sendability FROM messages WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + boolean found = rs.next(); + assert found; + int sendability = rs.getInt(1); + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + return sendability; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public Iterable getSendableMessages(Connection txn, + NeighbourId n, long capacity) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT size, messages.messageId FROM messages" + + " JOIN neighbourSubscriptions" + + " ON messages.groupId = neighbourSubscriptions.groupId" + + " JOIN statuses ON messages.messageId = statuses.messageId" + + " WHERE neighbourSubscriptions.neighbourId = ?" + + " AND statuses.neighbourId = ? AND status = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, n.getInt()); + ps.setInt(2, n.getInt()); + ps.setShort(3, (short) Status.NEW.ordinal()); + rs = ps.executeQuery(); + List ids = new ArrayList(); + long total = 0; + while(rs.next()) { + int size = rs.getInt(1); + if(total + size > capacity) break; + ids.add(new MessageId(rs.getBytes(2))); + total += size; + } + rs.close(); + ps.close(); + if(!ids.isEmpty()) { + System.out.println(ids.size() + " sendable messages, " + total + + " bytes"); + } + return ids; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public Set getSubscriptions(Connection txn) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT groupId FROM localSubscriptions"; + ps = txn.prepareStatement(sql); + rs = ps.executeQuery(); + Set ids = new HashSet(); + while(rs.next()) ids.add(new GroupId(rs.getBytes(1))); + rs.close(); + ps.close(); + return ids; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public Set removeBatchesToAck(Connection txn, NeighbourId n) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT batchId FROM batchesToAck" + + " WHERE neighbourId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, n.getInt()); + rs = ps.executeQuery(); + Set ids = new HashSet(); + while(rs.next()) ids.add(new BatchId(rs.getBytes(1))); + rs.close(); + ps.close(); + sql = "DELETE FROM batchesToAck WHERE neighbourId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, n.getInt()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == ids.size(); + ps.close(); + return ids; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public void removeLostBatch(Connection txn, NeighbourId n, BatchId b) + throws DbException { + PreparedStatement ps = null, ps1 = null; + ResultSet rs = null; + try { + String sql = "SELECT messageId FROM outstandingMessages" + + " WHERE neighbourId = ? AND batchId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, n.getInt()); + ps.setBytes(2, b.getBytes()); + rs = ps.executeQuery(); + sql = "UPDATE statuses SET status = ?" + + " WHERE messageId = ? AND neighbourId = ? AND status = ?"; + ps1 = txn.prepareStatement(sql); + ps1.setShort(1, (short) Status.NEW.ordinal()); + ps1.setInt(3, n.getInt()); + ps1.setShort(4, (short) Status.SENT.ordinal()); + int messages = 0; + while(rs.next()) { + messages++; + ps1.setBytes(2, rs.getBytes(1)); + ps1.addBatch(); + } + rs.close(); + ps.close(); + int[] rowsAffected = ps1.executeBatch(); + assert rowsAffected.length == messages; + for(int i = 0; i < rowsAffected.length; i++) { + assert rowsAffected[i] <= 1; + } + ps1.close(); + sql = "DELETE FROM outstandingBatches WHERE batchId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, b.getBytes()); + int rowsAffected1 = ps.executeUpdate(); + assert rowsAffected1 <= 1; + ps.close(); + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(ps1); + tryToClose(txn); + throw new DbException(e); + } + } + + public void removeMessage(Connection txn, MessageId m) throws DbException { + PreparedStatement ps = null; + try { + String sql = "DELETE FROM messages WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public Set removeOutstandingBatch(Connection txn, NeighbourId n, + BatchId b) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT messageId FROM outstandingMessages" + + " WHERE neighbourId = ? AND batchId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, n.getInt()); + ps.setBytes(2, b.getBytes()); + rs = ps.executeQuery(); + Set messages = new HashSet(); + while(rs.next()) messages.add(new MessageId(rs.getBytes(1))); + rs.close(); + ps.close(); + sql = "DELETE FROM outstandingBatches WHERE batchId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, b.getBytes()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected <= 1; + ps.close(); + return messages.isEmpty() ? null : messages; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public void removeSubscription(Connection txn, GroupId g) + throws DbException { + PreparedStatement ps = null; + try { + String sql = "DELETE FROM localSubscriptions WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public Rating setRating(Connection txn, AuthorId a, Rating r) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT rating FROM ratings WHERE authorId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, a.getBytes()); + rs = ps.executeQuery(); + Rating old; + if(rs.next()) { + old = Rating.values()[rs.getByte(1)]; + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + sql = "UPDATE ratings SET rating = ? WHERE authorId = ?"; + ps = txn.prepareStatement(sql); + ps.setShort(1, (short) r.ordinal()); + ps.setBytes(2, a.getBytes()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } else { + rs.close(); + ps.close(); + old = Rating.UNRATED; + sql = "INSERT INTO ratings (authorId, rating) VALUES (?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, a.getBytes()); + ps.setShort(2, (short) r.ordinal()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } + return old; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public void setSendability(Connection txn, MessageId m, int sendability) + throws DbException { + PreparedStatement ps = null; + try { + String sql = "UPDATE messages SET sendability = ?" + + " WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, sendability); + ps.setBytes(2, m.getBytes()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + + public void setStatus(Connection txn, NeighbourId n, MessageId m, Status s) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT status FROM statuses" + + " WHERE messageId = ? AND neighbourId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + ps.setInt(2, n.getInt()); + rs = ps.executeQuery(); + if(rs.next()) { + Status old = Status.values()[rs.getByte(1)]; + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + if(!old.equals(Status.SEEN) && !old.equals(s)) { + sql = "UPDATE statuses SET status = ?" + + " WHERE messageId = ? AND neighbourId = ?"; + ps = txn.prepareStatement(sql); + ps.setShort(1, (short) s.ordinal()); + ps.setBytes(2, m.getBytes()); + ps.setInt(3, n.getInt()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } + } else { + rs.close(); + ps.close(); + sql = "INSERT INTO statuses (messageId, neighbourId, status)" + + " VALUES (?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + ps.setInt(2, n.getInt()); + ps.setShort(3, (short) s.ordinal()); + int rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); + } + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } +} diff --git a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java new file mode 100644 index 000000000..1b5f7b100 --- /dev/null +++ b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java @@ -0,0 +1,497 @@ +package net.sf.briar.db; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.NeighbourId; +import net.sf.briar.api.db.Rating; +import net.sf.briar.api.db.Status; +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.Bundle; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { + + /* + * Locks must always be acquired in alphabetical order. See the Database + * interface to find out which calls require which locks. Note: this + * implementation can allow writers to starve. + */ + + private final ReentrantReadWriteLock messageLock = + new ReentrantReadWriteLock(true); + private final ReentrantReadWriteLock neighbourLock = + new ReentrantReadWriteLock(true); + private final ReentrantReadWriteLock ratingLock = + new ReentrantReadWriteLock(true); + private final ReentrantReadWriteLock subscriptionLock = + new ReentrantReadWriteLock(true); + + @Inject + ReadWriteLockDatabaseComponent(Database db, + Provider batchProvider) { + super(db, batchProvider); + } + + protected void expireMessages(long size) throws DbException { + messageLock.writeLock().lock(); + try { + neighbourLock.writeLock().lock(); + try { + Txn txn = db.startTransaction("cleaner"); + try { + for(MessageId m : db.getOldMessages(txn, size)) { + removeMessage(txn, m); + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + neighbourLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + } + + public void close() throws DbException { + messageLock.writeLock().lock(); + try { + neighbourLock.writeLock().lock(); + try { + ratingLock.writeLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + db.close(); + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + ratingLock.writeLock().unlock(); + } + } finally { + neighbourLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + } + + public void addNeighbour(NeighbourId n) throws DbException { + System.out.println("Adding neighbour " + n); + neighbourLock.writeLock().lock(); + try { + Txn txn = db.startTransaction("addNeighbour"); + try { + db.addNeighbour(txn, n); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + neighbourLock.writeLock().unlock(); + } + } + + public void addLocallyGeneratedMessage(Message m) throws DbException { + waitForPermissionToWrite(); + messageLock.writeLock().lock(); + try { + neighbourLock.writeLock().lock(); + try { + subscriptionLock.readLock().lock(); + try { + Txn txn = db.startTransaction("addLocallyGeneratedMessage"); + try { + if(db.containsSubscription(txn, m.getGroup())) { + boolean added = storeMessage(txn, m, null); + assert added; + } else { + System.out.println("Not subscribed"); + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + } finally { + neighbourLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + } + + public Rating getRating(AuthorId a) throws DbException { + ratingLock.readLock().lock(); + try { + Txn txn = db.startTransaction("getRating"); + try { + Rating r = db.getRating(txn, a); + db.commitTransaction(txn); + return r; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + ratingLock.readLock().unlock(); + } + } + + public void setRating(AuthorId a, Rating r) throws DbException { + messageLock.writeLock().lock(); + try { + ratingLock.writeLock().lock(); + try { + Txn txn = db.startTransaction("setRating"); + try { + Rating old = db.setRating(txn, a, r); + // Update the sendability of the author's messages + if(r == Rating.GOOD && old != Rating.GOOD) + updateAuthorSendability(txn, a, true); + else if(r != Rating.GOOD && old == Rating.GOOD) + updateAuthorSendability(txn, a, false); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + ratingLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + } + + public Set getSubscriptions() throws DbException { + subscriptionLock.readLock().lock(); + try { + Txn txn = db.startTransaction("getSubscriptions"); + try { + HashSet subs = new HashSet(); + for(GroupId g : db.getSubscriptions(txn)) subs.add(g); + db.commitTransaction(txn); + return subs; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + } + + public void subscribe(GroupId g) throws DbException { + System.out.println("Subscribing to " + g); + subscriptionLock.writeLock().lock(); + try { + Txn txn = db.startTransaction("subscribe"); + try { + db.addSubscription(txn, g); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } + + public void unsubscribe(GroupId g) throws DbException { + System.out.println("Unsubscribing from " + g); + messageLock.writeLock().lock(); + try { + neighbourLock.writeLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + Txn txn = db.startTransaction("unsubscribe"); + try { + db.removeSubscription(txn, g); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + neighbourLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + } + + public void generateBundle(NeighbourId n, Bundle b) throws DbException { + System.out.println("Generating bundle for " + n); + // Ack all batches received from the neighbour + neighbourLock.writeLock().lock(); + try { + Txn txn = db.startTransaction("generateBundle:acks"); + try { + int numAcks = 0; + for(BatchId ack : db.removeBatchesToAck(txn, n)) { + b.addAck(ack); + numAcks++; + } + System.out.println("Added " + numAcks + " acks"); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + neighbourLock.writeLock().unlock(); + } + // Add a list of subscriptions + subscriptionLock.readLock().lock(); + try { + Txn txn = db.startTransaction("generateBundle:subscriptions"); + try { + int numSubs = 0; + for(GroupId g : db.getSubscriptions(txn)) { + b.addSubscription(g); + numSubs++; + } + System.out.println("Added " + numSubs + " subscriptions"); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + // Add as many messages as possible to the bundle + long capacity = b.getCapacity(); + while(true) { + Batch batch = fillBatch(n, capacity); + if(batch == null) break; // No more messages to send + b.addBatch(batch); + capacity -= batch.getSize(); + // If the batch is less than half full, stop trying - there may be + // more messages trickling in but we can't wait forever + if(batch.getSize() * 2 < Batch.CAPACITY) break; + } + b.seal(); + System.out.println("Bundle sent, " + b.getSize() + " bytes"); + System.gc(); + } + + private Batch fillBatch(NeighbourId n, long capacity) throws DbException { + messageLock.readLock().lock(); + try { + Set sent; + Batch b; + neighbourLock.readLock().lock(); + try { + Txn txn = db.startTransaction("fillBatch:read"); + try { + capacity = Math.min(capacity, Batch.CAPACITY); + Iterator it = + db.getSendableMessages(txn, n, capacity).iterator(); + if(!it.hasNext()) { + db.commitTransaction(txn); + return null; // No more messages to send + } + sent = new HashSet(); + b = batchProvider.get(); + while(it.hasNext()) { + MessageId m = it.next(); + b.addMessage(db.getMessage(txn, m)); + sent.add(m); + } + b.seal(); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + neighbourLock.readLock().unlock(); + } + // Record the contents of the batch + neighbourLock.writeLock().lock(); + try { + Txn txn = db.startTransaction("fillBatch:write"); + try { + assert !sent.isEmpty(); + db.addOutstandingBatch(txn, n, b.getId(), sent); + db.commitTransaction(txn); + return b; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + neighbourLock.writeLock().unlock(); + } + } finally { + messageLock.readLock().unlock(); + } + } + + public void receiveBundle(NeighbourId n, Bundle b) throws DbException { + System.out.println("Received bundle from " + n + ", " + + b.getSize() + " bytes"); + // Mark all messages in acked batches as seen + messageLock.readLock().lock(); + try { + neighbourLock.writeLock().lock(); + try { + int acks = 0, expired = 0; + for(BatchId ack : b.getAcks()) { + acks++; + Txn txn = db.startTransaction("receiveBundle:acks"); + try { + Iterable batch = + db.removeOutstandingBatch(txn, n, ack); + // May be null if the batch was empty or has expired + if(batch == null) { + expired++; + } else { + for(MessageId m : batch) { + // Don't re-create statuses for expired messages + if(db.containsMessage(txn, m)) + db.setStatus(txn, n, m, Status.SEEN); + } + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + System.out.println("Received " + acks + " acks, " + expired + + " expired"); + } finally { + neighbourLock.writeLock().unlock(); + } + } finally { + messageLock.readLock().unlock(); + } + // Update the neighbour's subscriptions + neighbourLock.writeLock().lock(); + try { + Txn txn = db.startTransaction("receiveBundle:subscriptions"); + try { + db.clearSubscriptions(txn, n); + int subs = 0; + for(GroupId g : b.getSubscriptions()) { + subs++; + db.addSubscription(txn, n, g); + } + System.out.println("Received " + subs + " subscriptions"); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + neighbourLock.writeLock().unlock(); + } + // Store the messages + int batches = 0; + for(Batch batch : b.getBatches()) { + batches++; + waitForPermissionToWrite(); + messageLock.writeLock().lock(); + try { + neighbourLock.writeLock().lock(); + try { + subscriptionLock.readLock().lock(); + try { + Txn txn = db.startTransaction("receiveBundle:batch"); + try { + int received = 0, stored = 0; + for(Message m : batch.getMessages()) { + received++; + if(db.containsSubscription(txn, m.getGroup())) { + if(storeMessage(txn, m, n)) stored++; + } + } + System.out.println("Received " + received + + " messages, stored " + stored); + db.addBatchToAck(txn, n, batch.getId()); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + } finally { + neighbourLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + } + System.out.println("Received " + batches + " batches"); + // Find any lost batches that need to be retransmitted + Set lost; + messageLock.readLock().lock(); + try { + neighbourLock.writeLock().lock(); + try { + Txn txn = db.startTransaction("receiveBundle:findLost"); + try { + lost = db.addReceivedBundle(txn, n, b.getId()); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + neighbourLock.writeLock().unlock(); + } + } finally { + messageLock.readLock().unlock(); + } + for(BatchId batch : lost) { + messageLock.readLock().lock(); + try { + neighbourLock.writeLock().lock(); + try { + Txn txn = db.startTransaction("receiveBundle:removeLost"); + try { + System.out.println("Removing lost batch"); + db.removeLostBatch(txn, n, batch); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + neighbourLock.writeLock().unlock(); + } + } finally { + messageLock.readLock().unlock(); + } + } + System.gc(); + } +} \ No newline at end of file diff --git a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java new file mode 100644 index 000000000..b73af7a1a --- /dev/null +++ b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java @@ -0,0 +1,381 @@ +package net.sf.briar.db; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.NeighbourId; +import net.sf.briar.api.db.Rating; +import net.sf.briar.api.db.Status; +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.Bundle; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; + +class SynchronizedDatabaseComponent extends DatabaseComponentImpl { + + /* + * Locks must always be acquired in alphabetical order. See the Database + * interface to find out which calls require which locks. + */ + + private final Object messageLock = new Object(); + private final Object neighbourLock = new Object(); + private final Object ratingLock = new Object(); + private final Object subscriptionLock = new Object(); + + @Inject + SynchronizedDatabaseComponent(Database db, + Provider batchProvider) { + super(db, batchProvider); + } + + protected void expireMessages(long size) throws DbException { + synchronized(messageLock) { + synchronized(neighbourLock) { + Txn txn = db.startTransaction("cleaner"); + try { + for(MessageId m : db.getOldMessages(txn, size)) { + removeMessage(txn, m); + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + } + + public void close() throws DbException { + synchronized(messageLock) { + synchronized(neighbourLock) { + synchronized(ratingLock) { + synchronized(subscriptionLock) { + db.close(); + } + } + } + } + } + + public void addNeighbour(NeighbourId n) throws DbException { + System.out.println("Adding neighbour " + n); + synchronized(neighbourLock) { + Txn txn = db.startTransaction("addNeighbour"); + try { + db.addNeighbour(txn, n); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + + public void addLocallyGeneratedMessage(Message m) throws DbException { + waitForPermissionToWrite(); + synchronized(messageLock) { + synchronized(neighbourLock) { + synchronized(subscriptionLock) { + Txn txn = db.startTransaction("addLocallyGeneratedMessage"); + try { + if(db.containsSubscription(txn, m.getGroup())) { + boolean added = storeMessage(txn, m, null); + assert added; + } else { + System.out.println("Not subscribed"); + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + } + } + + public Rating getRating(AuthorId a) throws DbException { + synchronized(ratingLock) { + Txn txn = db.startTransaction("getRating"); + try { + Rating r = db.getRating(txn, a); + db.commitTransaction(txn); + return r; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + + public void setRating(AuthorId a, Rating r) throws DbException { + synchronized(messageLock) { + synchronized(ratingLock) { + Txn txn = db.startTransaction("setRating"); + try { + Rating old = db.setRating(txn, a, r); + // Update the sendability of the author's messages + if(r == Rating.GOOD && old != Rating.GOOD) + updateAuthorSendability(txn, a, true); + else if(r != Rating.GOOD && old == Rating.GOOD) + updateAuthorSendability(txn, a, false); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + } + + public Set getSubscriptions() throws DbException { + synchronized(subscriptionLock) { + Txn txn = db.startTransaction("getSubscriptions"); + try { + HashSet subs = new HashSet(); + for(GroupId g : db.getSubscriptions(txn)) subs.add(g); + db.commitTransaction(txn); + return subs; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + + public void subscribe(GroupId g) throws DbException { + System.out.println("Subscribing to " + g); + synchronized(subscriptionLock) { + Txn txn = db.startTransaction("subscribe"); + try { + db.addSubscription(txn, g); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + + public void unsubscribe(GroupId g) throws DbException { + System.out.println("Unsubscribing from " + g); + synchronized(messageLock) { + synchronized(neighbourLock) { + synchronized(subscriptionLock) { + Txn txn = db.startTransaction("unsubscribe"); + try { + db.removeSubscription(txn, g); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + } + } + + public void generateBundle(NeighbourId n, Bundle b) throws DbException { + System.out.println("Generating bundle for " + n); + // Ack all batches received from the neighbour + synchronized(neighbourLock) { + Txn txn = db.startTransaction("generateBundle:acks"); + try { + int numAcks = 0; + for(BatchId ack : db.removeBatchesToAck(txn, n)) { + b.addAck(ack); + numAcks++; + } + System.out.println("Added " + numAcks + " acks"); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + // Add a list of subscriptions + synchronized(subscriptionLock) { + Txn txn = db.startTransaction("generateBundle:subscriptions"); + try { + int numSubs = 0; + for(GroupId g : db.getSubscriptions(txn)) { + b.addSubscription(g); + numSubs++; + } + System.out.println("Added " + numSubs + " subscriptions"); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + // Add as many messages as possible to the bundle + long capacity = b.getCapacity(); + while(true) { + Batch batch = fillBatch(n, capacity); + if(batch == null) break; // No more messages to send + b.addBatch(batch); + capacity -= batch.getSize(); + // If the batch is less than half full, stop trying - there may be + // more messages trickling in but we can't wait forever + if(batch.getSize() * 2 < Batch.CAPACITY) break; + } + b.seal(); + System.out.println("Bundle sent, " + b.getSize() + " bytes"); + System.gc(); + } + + private Batch fillBatch(NeighbourId n, long capacity) throws DbException { + synchronized(messageLock) { + synchronized(neighbourLock) { + Txn txn = db.startTransaction("fillBatch"); + try { + capacity = Math.min(capacity, Batch.CAPACITY); + Iterator it = + db.getSendableMessages(txn, n, capacity).iterator(); + if(!it.hasNext()) { + db.commitTransaction(txn); + return null; // No more messages to send + } + Batch b = batchProvider.get(); + Set sent = new HashSet(); + while(it.hasNext()) { + MessageId m = it.next(); + b.addMessage(db.getMessage(txn, m)); + sent.add(m); + } + b.seal(); + // Record the contents of the batch + assert !sent.isEmpty(); + db.addOutstandingBatch(txn, n, b.getId(), sent); + db.commitTransaction(txn); + return b; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + } + + public void receiveBundle(NeighbourId n, Bundle b) throws DbException { + System.out.println("Received bundle from " + n + ", " + + b.getSize() + " bytes"); + // Mark all messages in acked batches as seen + synchronized(messageLock) { + synchronized(neighbourLock) { + int acks = 0, expired = 0; + for(BatchId ack : b.getAcks()) { + acks++; + Txn txn = db.startTransaction("receiveBundle:acks"); + try { + Iterable batch = + db.removeOutstandingBatch(txn, n, ack); + // May be null if the batch was empty or has expired + if(batch == null) { + expired++; + } else { + for(MessageId m : batch) { + // Don't re-create statuses for expired messages + if(db.containsMessage(txn, m)) + db.setStatus(txn, n, m, Status.SEEN); + } + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + System.out.println("Received " + acks + " acks, " + expired + + " expired"); + } + } + // Update the neighbour's subscriptions + synchronized(neighbourLock) { + Txn txn = db.startTransaction("receiveBundle:subscriptions"); + try { + db.clearSubscriptions(txn, n); + int subs = 0; + for(GroupId g : b.getSubscriptions()) { + subs++; + db.addSubscription(txn, n, g); + } + System.out.println("Received " + subs + " subscriptions"); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + // Store the messages + int batches = 0; + for(Batch batch : b.getBatches()) { + batches++; + waitForPermissionToWrite(); + synchronized(messageLock) { + synchronized(neighbourLock) { + synchronized(subscriptionLock) { + Txn txn = db.startTransaction("receiveBundle:batch"); + try { + int received = 0, stored = 0; + for(Message m : batch.getMessages()) { + received++; + if(db.containsSubscription(txn, m.getGroup())) { + if(storeMessage(txn, m, n)) stored++; + } + } + System.out.println("Received " + received + + " messages, stored " + stored); + db.addBatchToAck(txn, n, batch.getId()); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + } + } + System.out.println("Received " + batches + " batches"); + // Find any lost batches that need to be retransmitted + Set lost; + synchronized(messageLock) { + synchronized(neighbourLock) { + Txn txn = db.startTransaction("receiveBundle:findLost"); + try { + lost = db.addReceivedBundle(txn, n, b.getId()); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + for(BatchId batch : lost) { + synchronized(messageLock) { + synchronized(neighbourLock) { + Txn txn = db.startTransaction("receiveBundle:removeLost"); + try { + System.out.println("Removing lost batch"); + db.removeLostBatch(txn, n, batch); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + } + System.gc(); + } +} diff --git a/components/net/sf/briar/i18n/FontManagerImpl.java b/components/net/sf/briar/i18n/FontManagerImpl.java new file mode 100644 index 000000000..8b5f9623f --- /dev/null +++ b/components/net/sf/briar/i18n/FontManagerImpl.java @@ -0,0 +1,99 @@ +package net.sf.briar.i18n; + +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.font.TextAttribute; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; + +import javax.swing.UIManager; + +import net.sf.briar.api.i18n.FontManager; +import net.sf.briar.util.FileUtils; + +public class FontManagerImpl implements FontManager { + + private static final BundledFont[] BUNDLED_FONTS = { + new BundledFont("TibetanMachineUni.ttf", 14f, new String[] { "bo" }), + new BundledFont("Padauk.ttf", 14f, new String[] { "my" }), + }; + + private final Map fonts = new TreeMap(); + + private volatile Font defaultFont = null, uiFont = null; + + public void initialize(Locale locale) throws IOException { + try { + ClassLoader loader = getClass().getClassLoader(); + for(BundledFont bf : BUNDLED_FONTS) { + InputStream in = loader.getResourceAsStream(bf.filename); + if(in == null) { + File root = FileUtils.getBriarDirectory(); + File file = new File(root, "Data/" + bf.filename); + in = new FileInputStream(file); + } + Font font = Font.createFont(Font.TRUETYPE_FONT, in); + font = font.deriveFont(bf.size); + for(String language : bf.languages) fonts.put(language, font); + } + } catch(FontFormatException e) { + throw new IOException(e); + } + defaultFont = getFont("Sans", 12f); + assert defaultFont != null; // FIXME: This is failing on Windows + setUiFontForLanguage(locale.getLanguage()); + } + + private Font getFont(String name, float size) { + Map attr = new HashMap(); + attr.put(TextAttribute.FAMILY, name); + attr.put(TextAttribute.SIZE, Float.valueOf(size)); + return Font.getFont(attr); + } + + public String[] getBundledFontFilenames() { + String[] names = new String[BUNDLED_FONTS.length]; + for(int i = 0; i < BUNDLED_FONTS.length; i++) + names[i] = BUNDLED_FONTS[i].filename; + return names; + } + + public Font getFontForLanguage(String language) { + assert defaultFont != null; + Font font = fonts.get(language); + return font == null ? defaultFont : font; + } + + public Font getUiFont() { + return uiFont; + } + + public void setUiFontForLanguage(String language) { + uiFont = getFontForLanguage(language); + Enumeration keys = UIManager.getDefaults().keys(); + while(keys.hasMoreElements()) { + Object key = keys.nextElement(); + if(UIManager.getFont(key) != null) UIManager.put(key, uiFont); + } + } + + private static class BundledFont { + + private final String filename; + private final float size; + private final String[] languages; + + BundledFont(String filename, float size, String[] languages) { + this.filename = filename; + this.size = size; + this.languages = languages; + } + } +} diff --git a/components/net/sf/briar/i18n/I18nImpl.java b/components/net/sf/briar/i18n/I18nImpl.java new file mode 100644 index 000000000..af09abf9d --- /dev/null +++ b/components/net/sf/briar/i18n/I18nImpl.java @@ -0,0 +1,152 @@ +package net.sf.briar.i18n; + +import java.awt.ComponentOrientation; +import java.awt.Font; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.HashSet; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.Scanner; +import java.util.Set; + +import javax.swing.UIManager; + +import com.google.inject.Inject; + +import net.sf.briar.api.i18n.FontManager; +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.util.FileUtils; + +public class I18nImpl implements I18n { + + private static final String[] uiManagerKeys = { + "FileChooser.acceptAllFileFilterText", + "FileChooser.cancelButtonText", + "FileChooser.cancelButtonToolTipText", + "FileChooser.detailsViewButtonAccessibleName", + "FileChooser.detailsViewButtonToolTipText", + "FileChooser.directoryOpenButtonText", + "FileChooser.directoryOpenButtonToolTipText", + "FileChooser.fileAttrHeaderText", + "FileChooser.fileDateHeaderText", + "FileChooser.fileNameHeaderText", + "FileChooser.fileNameLabelText", + "FileChooser.fileSizeHeaderText", + "FileChooser.filesOfTypeLabelText", + "FileChooser.fileTypeHeaderText", + "FileChooser.helpButtonText", + "FileChooser.helpButtonToolTipText", + "FileChooser.homeFolderAccessibleName", + "FileChooser.homeFolderToolTipText", + "FileChooser.listViewButtonAccessibleName", + "FileChooser.listViewButtonToolTipText", + "FileChooser.lookInLabelText", + "FileChooser.newFolderErrorText", + "FileChooser.newFolderToolTipText", + "FileChooser.openButtonText", + "FileChooser.openButtonToolTipText", + "FileChooser.openDialogTitleText", + "FileChooser.saveButtonText", + "FileChooser.saveButtonToolTipText", + "FileChooser.saveDialogTitleText", + "FileChooser.saveInLabelText", + "FileChooser.updateButtonText", + "FileChooser.updateButtonToolTipText", + "FileChooser.upFolderAccessibleName", + "FileChooser.upFolderToolTipText", + "OptionPane.cancelButtonText", + "OptionPane.noButtonText", + "OptionPane.yesButtonText", + "ProgressMonitor.progressText" + }; + + private final Object bundleLock = new Object(); + private final ClassLoader loader = I18n.class.getClassLoader(); + private final Set listeners = new HashSet(); + private final FontManager fontManager; + + private volatile Locale locale = Locale.getDefault(); + private volatile ResourceBundle bundle = null; + + @Inject + public I18nImpl(FontManager fontManager) { + this.fontManager = fontManager; + } + + public String tr(String name) { + loadResourceBundle(); + return bundle.getString(name); + } + + private void loadResourceBundle() { + if(bundle == null) { + synchronized(bundleLock) { + if(bundle == null) { + bundle = ResourceBundle.getBundle("i18n", locale, loader); + for(String key : uiManagerKeys) { + try { + UIManager.put(key, bundle.getString(key)); + } catch(MissingResourceException ignored) {} + } + } + } + } + } + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + fontManager.setUiFontForLanguage(locale.getLanguage()); + Font uiFont = fontManager.getUiFont(); + synchronized(bundleLock) { + this.locale = locale; + bundle = null; + synchronized(listeners) { + for(Listener l : listeners) l.localeChanged(uiFont); + } + } + } + + public void loadLocale() throws IOException { + File root = FileUtils.getBriarDirectory(); + Scanner s = new Scanner(new File(root, "Data/locale.cfg")); + if(s.hasNextLine()) locale = new Locale(s.nextLine()); + s.close(); + } + + public void saveLocale() throws IOException { + saveLocale(FileUtils.getBriarDirectory()); + } + + public void saveLocale(File dir) throws IOException { + File localeCfg = new File(dir, "locale.cfg"); + FileOutputStream out = new FileOutputStream(localeCfg); + PrintStream print = new PrintStream(out); + print.println(locale); + print.flush(); + print.close(); + } + + public ComponentOrientation getComponentOrientation() { + return ComponentOrientation.getOrientation(locale); + } + + public void addListener(Listener l) { + l.localeChanged(fontManager.getUiFont()); + synchronized(listeners) { + listeners.add(l); + } + } + + public void removeListener(Listener l) { + synchronized(listeners) { + listeners.remove(l); + } + } +} diff --git a/components/net/sf/briar/i18n/I18nModule.java b/components/net/sf/briar/i18n/I18nModule.java new file mode 100644 index 000000000..1c51259d6 --- /dev/null +++ b/components/net/sf/briar/i18n/I18nModule.java @@ -0,0 +1,16 @@ +package net.sf.briar.i18n; + +import net.sf.briar.api.i18n.FontManager; +import net.sf.briar.api.i18n.I18n; + +import com.google.inject.AbstractModule; +import com.google.inject.Singleton; + +public class I18nModule extends AbstractModule { + + @Override + protected void configure() { + bind(FontManager.class).to(FontManagerImpl.class).in(Singleton.class); + bind(I18n.class).to(I18nImpl.class).in(Singleton.class); + } +} diff --git a/components/net/sf/briar/invitation/InvitationModule.java b/components/net/sf/briar/invitation/InvitationModule.java new file mode 100644 index 000000000..021aee31d --- /dev/null +++ b/components/net/sf/briar/invitation/InvitationModule.java @@ -0,0 +1,13 @@ +package net.sf.briar.invitation; + +import net.sf.briar.api.invitation.InvitationWorkerFactory; + +import com.google.inject.AbstractModule; + +public class InvitationModule extends AbstractModule { + + @Override + protected void configure() { + bind(InvitationWorkerFactory.class).to(InvitationWorkerFactoryImpl.class); + } +} diff --git a/components/net/sf/briar/invitation/InvitationWorker.java b/components/net/sf/briar/invitation/InvitationWorker.java new file mode 100644 index 000000000..4c8f94e55 --- /dev/null +++ b/components/net/sf/briar/invitation/InvitationWorker.java @@ -0,0 +1,104 @@ +package net.sf.briar.invitation; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import net.sf.briar.api.invitation.InvitationCallback; +import net.sf.briar.api.invitation.InvitationParameters; +import net.sf.briar.util.FileUtils; + +class InvitationWorker implements Runnable { + + private final InvitationCallback callback; + private final InvitationParameters parameters; + + InvitationWorker(final InvitationCallback callback, + InvitationParameters parameters) { + this.callback = callback; + this.parameters = parameters; + } + + public void run() { + File dir = parameters.getChosenLocation(); + assert dir != null; + if(!dir.exists()) { + callback.notFound(dir); + return; + } + if(!dir.isDirectory()) { + callback.notDirectory(dir); + return; + } + if(!dir.canWrite()) { + callback.notAllowed(dir); + return; + } + List files = new ArrayList(); + try { + if(callback.isCancelled()) return; + File invitationDat = createInvitationDat(dir); + files.add(invitationDat); + if(callback.isCancelled()) return; + if(parameters.shouldCreateExe()) { + File briarExe = createBriarExe(dir); + files.add(briarExe); + } + if(callback.isCancelled()) return; + if(parameters.shouldCreateJar()) { + File briarJar = createBriarJar(dir); + files.add(briarJar); + } + } catch(IOException e) { + callback.error(e.getMessage()); + return; + } + if(callback.isCancelled()) return; + callback.created(files); + } + + private File createInvitationDat(File dir) throws IOException { + char[] password = parameters.getPassword(); + assert password != null; + File invitationDat = new File(dir, "invitation.dat"); + callback.encryptingFile(invitationDat); + // FIXME: Create a real invitation + try { + Thread.sleep(2000); + } catch(InterruptedException ignored) { + } + Arrays.fill(password, (char) 0); + FileOutputStream out = new FileOutputStream(invitationDat); + byte[] buf = new byte[1024]; + new Random().nextBytes(buf); + out.write(buf, 0, buf.length); + out.flush(); + out.close(); + return invitationDat; + } + + private File createBriarExe(File dir) throws IOException { + File f = new File(dir, "briar.exe"); + copyInstaller(f); + return f; + } + + private File createBriarJar(File dir) throws IOException { + File f = new File(dir, "briar.jar"); + copyInstaller(f); + return f; + } + + private void copyInstaller(File dest) throws IOException { + File root = FileUtils.getBriarDirectory(); + File src = new File(root, "Data/setup.dat"); + if(!src.exists() || !src.isFile()) + throw new IOException("File not found: " + src.getPath()); + callback.copyingFile(dest); + FileUtils.copy(src, dest); + } +} \ No newline at end of file diff --git a/components/net/sf/briar/invitation/InvitationWorkerFactoryImpl.java b/components/net/sf/briar/invitation/InvitationWorkerFactoryImpl.java new file mode 100644 index 000000000..09eb650b5 --- /dev/null +++ b/components/net/sf/briar/invitation/InvitationWorkerFactoryImpl.java @@ -0,0 +1,13 @@ +package net.sf.briar.invitation; + +import net.sf.briar.api.invitation.InvitationCallback; +import net.sf.briar.api.invitation.InvitationParameters; +import net.sf.briar.api.invitation.InvitationWorkerFactory; + +class InvitationWorkerFactoryImpl implements InvitationWorkerFactory { + + public Runnable createWorker(InvitationCallback callback, + InvitationParameters parameters) { + return new InvitationWorker(callback, parameters); + } +} diff --git a/components/net/sf/briar/protocol/BatchImpl.java b/components/net/sf/briar/protocol/BatchImpl.java new file mode 100644 index 000000000..a6c5eb4a6 --- /dev/null +++ b/components/net/sf/briar/protocol/BatchImpl.java @@ -0,0 +1,40 @@ +package net.sf.briar.protocol; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.Message; + +class BatchImpl implements Batch { + + private final List messages = new ArrayList(); + private BatchId id = null; + private long size = 0L; + + public void seal() { + System.out.println("FIXME: Calculate batch ID"); + byte[] b = new byte[BatchId.LENGTH]; + new Random().nextBytes(b); + id = new BatchId(b); + } + + public BatchId getId() { + return id; + } + + public long getSize() { + return size; + } + + public Iterable getMessages() { + return messages; + } + + public void addMessage(Message m) { + messages.add(m); + size += m.getSize(); + } +} diff --git a/components/net/sf/briar/protocol/MessageImpl.java b/components/net/sf/briar/protocol/MessageImpl.java new file mode 100644 index 000000000..7f8c92c39 --- /dev/null +++ b/components/net/sf/briar/protocol/MessageImpl.java @@ -0,0 +1,63 @@ +package net.sf.briar.protocol; + +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; + +class MessageImpl implements Message { + + private final MessageId id, parent; + private final GroupId group; + private final AuthorId author; + private final long timestamp; + private final byte[] body; + + public MessageImpl(MessageId id, MessageId parent, GroupId group, + AuthorId author, long timestamp, byte[] body) { + this.id = id; + this.parent = parent; + this.group = group; + this.author = author; + this.timestamp = timestamp; + this.body = body; + } + + public MessageId getId() { + return id; + } + + public MessageId getParent() { + return parent; + } + + public GroupId getGroup() { + return group; + } + + public AuthorId getAuthor() { + return author; + } + + public long getTimestamp() { + return timestamp; + } + + public int getSize() { + return body.length; + } + + public byte[] getBody() { + return body; + } + + @Override + public boolean equals(Object o) { + return o instanceof MessageImpl && id.equals(((MessageImpl)o).id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/components/net/sf/briar/protocol/ProtocolModule.java b/components/net/sf/briar/protocol/ProtocolModule.java new file mode 100644 index 000000000..30d41ee82 --- /dev/null +++ b/components/net/sf/briar/protocol/ProtocolModule.java @@ -0,0 +1,20 @@ +package net.sf.briar.protocol; + +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.Message; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; + +public class ProtocolModule extends AbstractModule { + + @Override + protected void configure() { + bind(Message.class).to(MessageImpl.class); + } + + @Provides + Batch createBatch() { + return new BatchImpl(); + } +} diff --git a/components/net/sf/briar/setup/SetupModule.java b/components/net/sf/briar/setup/SetupModule.java new file mode 100644 index 000000000..71dd5b0ea --- /dev/null +++ b/components/net/sf/briar/setup/SetupModule.java @@ -0,0 +1,13 @@ +package net.sf.briar.setup; + +import net.sf.briar.api.setup.SetupWorkerFactory; + +import com.google.inject.AbstractModule; + +public class SetupModule extends AbstractModule { + + @Override + protected void configure() { + bind(SetupWorkerFactory.class).to(SetupWorkerFactoryImpl.class); + } +} diff --git a/components/net/sf/briar/setup/SetupWorker.java b/components/net/sf/briar/setup/SetupWorker.java new file mode 100644 index 000000000..ccfa490ac --- /dev/null +++ b/components/net/sf/briar/setup/SetupWorker.java @@ -0,0 +1,157 @@ +package net.sf.briar.setup; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.security.CodeSource; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.setup.SetupCallback; +import net.sf.briar.api.setup.SetupParameters; +import net.sf.briar.util.FileUtils; +import net.sf.briar.util.OsUtils; +import net.sf.briar.util.ZipUtils; + +class SetupWorker implements Runnable { + + private static final String MAIN_CLASS = + "net.sf.briar.ui.invitation.InvitationMain"; + private static final int EXE_HEADER_SIZE = 62976; + + private final SetupCallback callback; + private final SetupParameters parameters; + private final I18n i18n; + private final ZipUtils.Callback unzipCallback; + + SetupWorker(final SetupCallback callback, SetupParameters parameters, + I18n i18n) { + this.parameters = parameters; + this.callback = callback; + this.i18n = i18n; + unzipCallback = new ZipUtils.Callback() { + public void processingFile(File f) { + callback.extractingFile(f); + } + }; + } + + public void run() { + File dir = parameters.getChosenLocation(); + assert dir != null; + if(!dir.exists()) { + callback.notFound(dir); + return; + } + if(!dir.isDirectory()) { + callback.notDirectory(dir); + return; + } + String[] list = dir.list(); + if(list == null || !dir.canWrite()) { + callback.notAllowed(dir); + return; + } + if(list.length != 0) { + dir = new File(dir, "Briar"); + if(!dir.exists() && !dir.mkdir()) { + callback.notAllowed(dir); + return; + } + } + File data = new File(dir, "Data"); + if(!data.exists() && !data.mkdir()) { + callback.notAllowed(data); + return; + } + try { + if(callback.isCancelled()) return; + File jar = getJar(); + if(callback.isCancelled()) return; + copyInstaller(jar, data); + if(callback.isCancelled()) return; + extractFiles(jar, data, "^jre/.*|.*\\.jar$|.*\\.ttf$"); + if(callback.isCancelled()) return; + createLaunchers(dir); + if(callback.isCancelled()) return; + i18n.saveLocale(data); + if(callback.isCancelled()) return; + jar.deleteOnExit(); + } catch(IOException e) { + callback.error(e.getMessage()); + return; + } + if(callback.isCancelled()) return; + callback.installed(dir); + } + + private File getJar() throws IOException { + CodeSource c = FileUtils.class.getProtectionDomain().getCodeSource(); + File jar = new File(c.getLocation().getPath()); + assert jar.exists(); + if(!jar.isFile()) throw new IOException("Not running from a jar"); + return jar; + } + + private void copyInstaller(File jar, File dir) throws IOException { + File dest = new File(dir, "setup.dat"); + callback.copyingFile(dest); + FileUtils.copy(jar, dest); + } + + private void extractFiles(File jar, File dir, String regex) + throws IOException { + FileInputStream in = new FileInputStream(jar); + in.skip(EXE_HEADER_SIZE); + ZipUtils.unzipStream(in, dir, regex, unzipCallback); + } + + private void createLaunchers(File dir) throws IOException { + createWindowsLauncher(dir); + File mac = createMacLauncher(dir); + File lin = createLinuxLauncher(dir); + if(!OsUtils.isWindows()) { + String[] chmod = { "chmod", "u+x", mac.getName(), lin.getName() }; + ProcessBuilder p = new ProcessBuilder(chmod); + p.directory(dir); + p.start(); + } + } + + private File createWindowsLauncher(File dir) throws IOException { + File launcher = new File(dir, "run-windows.vbs"); + PrintStream out = new PrintStream(new FileOutputStream(launcher)); + out.print("Set Shell = CreateObject(\"WScript.Shell\")\r\n"); + out.print("Shell.Run \"Data\\jre\\bin\\javaw -ea -cp Data\\* " + + MAIN_CLASS + "\", 0\r\n"); + out.print("Set Shell = Nothing\r\n"); + out.flush(); + out.close(); + return launcher; + } + + // FIXME: If this pops up a terminal window, the Mac launcher may need + // to be a jar + private File createMacLauncher(File dir) throws IOException { + File launcher = new File(dir, "run-mac.command"); + PrintStream out = new PrintStream(new FileOutputStream(launcher)); + out.print("#!/bin/sh\n"); + out.print("cd \"$(dirname \"$0\")\"\n"); + out.print("java -ea -cp 'Data/*' " + MAIN_CLASS + "\n"); + out.flush(); + out.close(); + return launcher; + } + + private File createLinuxLauncher(File dir) throws IOException { + File launcher = new File(dir, "run-linux.sh"); + PrintStream out = new PrintStream(new FileOutputStream(launcher)); + out.print("#!/bin/sh\n"); + out.print("cd \"$(dirname \"$0\")\"\n"); + out.print("java -ea -cp 'Data/*' " + MAIN_CLASS + "\n"); + out.flush(); + out.close(); + return launcher; + } +} diff --git a/components/net/sf/briar/setup/SetupWorkerFactoryImpl.java b/components/net/sf/briar/setup/SetupWorkerFactoryImpl.java new file mode 100644 index 000000000..51425399c --- /dev/null +++ b/components/net/sf/briar/setup/SetupWorkerFactoryImpl.java @@ -0,0 +1,23 @@ +package net.sf.briar.setup; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.setup.SetupCallback; +import net.sf.briar.api.setup.SetupParameters; +import net.sf.briar.api.setup.SetupWorkerFactory; + +import com.google.inject.Inject; + +public class SetupWorkerFactoryImpl implements SetupWorkerFactory { + + private final I18n i18n; + + @Inject + public SetupWorkerFactoryImpl(I18n i18n) { + this.i18n = i18n; + } + + public Runnable createWorker(SetupCallback callback, + SetupParameters parameters) { + return new SetupWorker(callback, parameters, i18n); + } +} diff --git a/dependencies.xml b/dependencies.xml new file mode 100644 index 000000000..f6b2af378 --- /dev/null +++ b/dependencies.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/i18n/Padauk.ttf b/i18n/Padauk.ttf new file mode 100644 index 000000000..cd1d113d3 Binary files /dev/null and b/i18n/Padauk.ttf differ diff --git a/i18n/TibetanMachineUni.ttf b/i18n/TibetanMachineUni.ttf new file mode 100644 index 000000000..d9e1899fc Binary files /dev/null and b/i18n/TibetanMachineUni.ttf differ diff --git a/i18n/i18n.properties b/i18n/i18n.properties new file mode 100644 index 000000000..2e7329e29 --- /dev/null +++ b/i18n/i18n.properties @@ -0,0 +1,73 @@ +BACK=Back +NEXT=Next +CANCEL=Cancel +FINISH=Finish + +YES=Yes +NO=No +UNKNOWN=Unknown + +CANCELLING=Cancelling, please wait... + +ENCRYPTING_FILE=Encrypting file: +EXTRACTING_FILE=Extracting file: +COPYING_FILE=Copying file: + +DIRECTORY_NOT_FOUND=The chosen folder was not found: +FILE_NOT_DIRECTORY=The chosen location is not a folder: +DIRECTORY_NOT_ALLOWED=You do not have permission to use the chosen folder: + +WINDOWS=Windows +MAC=Macintosh +LINUX=Linux + +ENTER_PASSWORD=Enter password: +CONFIRM_PASSWORD=Confirm password: + +INVITATION_TITLE=Create Invitation +INVITATION_INTRO=\ +This wizard will guide you through the process of inviting a new contact to \ +join Briar.

\ +The wizard will create some files that you must give to your contact.

\ +You will also be asked to choose a password, which your contact will use to \ +unlock the invitation. +INVITATION_EXISTING_USER=Does your contact already use Briar? +INVITATION_OPERATING_SYSTEM=What kind of computer does your contact use? +INVITATION_PASSWORD=\ +Please choose a password for the invitation.

\ +Your contact will need this password to unlock the invitation.

\ +It is very important that you DO NOT send this password across the internet \ +or by SMS. +INVITATION_LOCATION_TEXT=\ +Please choose where to save the invitation files.

\ +It is recommended to save them on a removable device such as a USB stick. \ +Alternatively, you can send them to your contact by Bluetooth.

\ +Please press Next to choose a location. +INVITATION_LOCATION_TITLE=Save Invitation Files +INVITATION_PROGRESS_BEGIN=Preparing to create invitation... +INVITATION_CREATED=The following files have been created: +INVITATION_GIVE_TO_CONTACT=\ +Please give these files and the password to your contact. +INVITATION_ERROR=An error occurred while creating the invitation: +INVITATION_ABORTED=The invitation could not be not created. + +SETUP_TITLE=Setup +SETUP_LANGUAGE=\ +Welcome to the setup program for Briar, a secure news and discussion \ +network.

\ +Please choose a language and press Next to begin the installation. +SETUP_ALREADY_INSTALLED=Are you already a user of Briar? +SETUP_INSTRUCTIONS=\ +To accept the person who sent you this invitation as a contact, please open \ +the invitation.dat file in Briar by selecting File > Open from the menu, or \ +by dragging the file onto the Briar window. +SETUP_LOCATION_TEXT=\ +It is recommended to install Briar on a removable device such as a USB \ +stick.

\ +Please press Next to choose the folder where Briar will be installed. +SETUP_LOCATION_TITLE=Choose Folder +SETUP_PROGRESS_BEGIN=Preparing to install... +SETUP_INSTALLED=Briar has been installed in the following folder: +SETUP_UNINSTALL=To uninstall Briar, simply delete the folder. +SETUP_ERROR=An error occurred while installing: +SETUP_ABORTED=The installation could not be completed. diff --git a/i18n/i18n_bo.properties b/i18n/i18n_bo.properties new file mode 100644 index 000000000..77bd11893 --- /dev/null +++ b/i18n/i18n_bo.properties @@ -0,0 +1,2 @@ +# Note: It seems to be necessary to insert a zero-width space (\u200b) after +# every inter-word dot (\u0f0b) to allow line-breaking. diff --git a/installer/net/sf/briar/ui/setup/SetupMain.java b/installer/net/sf/briar/ui/setup/SetupMain.java new file mode 100644 index 000000000..db25a705b --- /dev/null +++ b/installer/net/sf/briar/ui/setup/SetupMain.java @@ -0,0 +1,37 @@ +package net.sf.briar.ui.setup; + +import java.util.Locale; + +import javax.swing.UIManager; + +import net.sf.briar.api.i18n.FontManager; +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.setup.SetupParameters; +import net.sf.briar.api.setup.SetupWorkerFactory; +import net.sf.briar.i18n.FontManagerImpl; +import net.sf.briar.i18n.I18nImpl; +import net.sf.briar.setup.SetupWorkerFactoryImpl; +import net.sf.briar.util.OsUtils; + +public class SetupMain { + + public static void main(String[] args) throws Exception { + if(OsUtils.isWindows() || OsUtils.isMac()) + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + + FontManager fontManager = new FontManagerImpl(); + I18n i18n = new I18nImpl(fontManager); + SetupWorkerFactory workerFactory = new SetupWorkerFactoryImpl(i18n); + SetupWizard wizard = new SetupWizard(i18n); + new LanguagePanel(wizard, fontManager, i18n); + new AlreadyInstalledPanel(wizard, i18n); + new InstructionsPanel(wizard, i18n); + LocationPanel locationPanel = new LocationPanel(wizard, i18n); + SetupParameters parameters = + new SetupParametersImpl(locationPanel, fontManager); + new SetupWorkerPanel(wizard, workerFactory, parameters, i18n); + + fontManager.initialize(Locale.getDefault()); + wizard.display(); + } +} diff --git a/lib/guice-3.0-no_aop.jar b/lib/guice-3.0-no_aop.jar new file mode 100644 index 000000000..ef3a55b0c Binary files /dev/null and b/lib/guice-3.0-no_aop.jar differ diff --git a/lib/h2small-1.3.154.jar b/lib/h2small-1.3.154.jar new file mode 100644 index 000000000..dbdafab1f Binary files /dev/null and b/lib/h2small-1.3.154.jar differ diff --git a/lib/installer.manifest b/lib/installer.manifest new file mode 100644 index 000000000..fa91ff510 --- /dev/null +++ b/lib/installer.manifest @@ -0,0 +1,2 @@ +Main-Class: net.sf.briar.ui.setup.SetupMain + diff --git a/lib/javax.inject-1.jar b/lib/javax.inject-1.jar new file mode 100644 index 000000000..b2a9d0bf7 Binary files /dev/null and b/lib/javax.inject-1.jar differ diff --git a/lib/setup.vbs b/lib/setup.vbs new file mode 100644 index 000000000..b53d3a8f2 --- /dev/null +++ b/lib/setup.vbs @@ -0,0 +1,7 @@ +Set Shell = CreateObject("WScript.Shell") +Shell.Run "briar.tmp\jre\bin\javaw -ea -jar briar.exe", 0, true +Set Shell = Nothing +Set Fso = CreateObject("Scripting.FileSystemObject") +Fso.DeleteFolder "briar.tmp" +Set Fso = Nothing + diff --git a/lib/unzipsfx.exe b/lib/unzipsfx.exe new file mode 100755 index 000000000..a101bac27 Binary files /dev/null and b/lib/unzipsfx.exe differ diff --git a/make-installer.sh b/make-installer.sh new file mode 100755 index 000000000..3c8504ffe --- /dev/null +++ b/make-installer.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +#FIXME: Replace this with an ant script + +rm -rf temp briar.zip +mkdir temp +cd bin +for dir in api/i18n api/setup i18n setup util ui/setup ui/wizard +do + mkdir -p ../temp/net/sf/briar/$dir + cp net/sf/briar/$dir/*.class ../temp/net/sf/briar/$dir +done +jar cf ../temp/main.jar net *.properties +cd .. +cp i18n/*.properties i18n/*.ttf temp +cp lib/*.jar temp +cp -r windows-jre temp/jre +cp lib/setup.vbs temp +mkdir temp/META-INF +cp lib/installer.manifest temp/META-INF/MANIFEST.MF +cd temp +echo '$AUTORUN$>start /b briar.tmp\\setup.vbs' | zip -z -r ../briar.zip META-INF net jre *.jar *.properties *.ttf setup.vbs +cd .. +cat lib/unzipsfx.exe briar.zip > briar.exe +rm -rf temp briar.zip diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1 @@ +/build diff --git a/ui/build.xml b/ui/build.xml new file mode 100644 index 000000000..9190ee924 --- /dev/null +++ b/ui/build.xml @@ -0,0 +1,3 @@ + + + diff --git a/ui/net/sf/briar/ui/invitation/ExistingUserPanel.java b/ui/net/sf/briar/ui/invitation/ExistingUserPanel.java new file mode 100644 index 000000000..0f54cf028 --- /dev/null +++ b/ui/net/sf/briar/ui/invitation/ExistingUserPanel.java @@ -0,0 +1,94 @@ +package net.sf.briar.ui.invitation; + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridLayout; + +import javax.swing.ButtonGroup; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.SwingConstants; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.ui.wizard.Wizard; +import net.sf.briar.ui.wizard.WizardPanel; + +class ExistingUserPanel extends WizardPanel { + + private static final long serialVersionUID = -8536392615847105689L; + + private final Stri18ng question, yes, no, unknown; + private final JLabel label; + private final JRadioButton yesButton, noButton, unknownButton; + + ExistingUserPanel(Wizard wizard, I18n i18n) { + super(wizard, "ExistingUser"); + question = new Stri18ng("INVITATION_EXISTING_USER", i18n); + yes = new Stri18ng("YES", i18n); + no = new Stri18ng("NO", i18n); + unknown = new Stri18ng("UNKNOWN", i18n); + label = new JLabel(question.html()); + Dimension d = wizard.getPreferredSize(); + label.setPreferredSize(new Dimension(d.width - 50, 50)); + label.setVerticalAlignment(SwingConstants.TOP); + add(label); + yesButton = new JRadioButton(yes.tr()); + noButton = new JRadioButton(no.tr()); + unknownButton = new JRadioButton(unknown.tr()); + ButtonGroup group = new ButtonGroup(); + group.add(yesButton); + group.add(noButton); + group.add(unknownButton); + unknownButton.setSelected(true); + JPanel buttonPanel = new JPanel(new GridLayout(3, 1)); + buttonPanel.add(yesButton); + buttonPanel.add(noButton); + buttonPanel.add(unknownButton); + add(buttonPanel); + } + + public void localeChanged(Font uiFont) { + label.setText(question.html()); + label.setFont(uiFont); + yesButton.setText(yes.tr()); + yesButton.setFont(uiFont); + noButton.setText(no.tr()); + noButton.setFont(uiFont); + unknownButton.setText(unknown.tr()); + unknownButton.setFont(uiFont); + } + + @Override + protected void display() { + wizard.setBackButtonEnabled(true); + wizard.setNextButtonEnabled(true); + wizard.setFinished(false); + } + + @Override + protected void backButtonPressed() { + wizard.showPanel("Intro"); + } + + @Override + protected void nextButtonPressed() { + if(shouldCreateInstaller()) wizard.showPanel("OperatingSystem"); + else wizard.showPanel("Password"); + } + + @Override + protected void cancelButtonPressed() { + wizard.close(); + } + + @Override + protected void finishButtonPressed() { + assert false; + } + + boolean shouldCreateInstaller() { + return !yesButton.isSelected(); + } +} diff --git a/ui/net/sf/briar/ui/invitation/IntroPanel.java b/ui/net/sf/briar/ui/invitation/IntroPanel.java new file mode 100644 index 000000000..764b064b1 --- /dev/null +++ b/ui/net/sf/briar/ui/invitation/IntroPanel.java @@ -0,0 +1,42 @@ +package net.sf.briar.ui.invitation; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.ui.wizard.TextPanel; +import net.sf.briar.ui.wizard.Wizard; + +class IntroPanel extends TextPanel { + + private static final long serialVersionUID = 2428034340183141779L; + + IntroPanel(Wizard wizard, I18n i18n) { + super(wizard, "Intro", new Stri18ng("INVITATION_INTRO", i18n)); + } + + @Override + protected void display() { + wizard.setBackButtonEnabled(false); + wizard.setNextButtonEnabled(true); + wizard.setFinished(false); + } + + @Override + protected void backButtonPressed() { + assert false; + } + + @Override + protected void nextButtonPressed() { + wizard.showPanel("ExistingUser"); + } + + @Override + protected void cancelButtonPressed() { + wizard.close(); + } + + @Override + protected void finishButtonPressed() { + assert false; + } +} diff --git a/ui/net/sf/briar/ui/invitation/InvitationParametersImpl.java b/ui/net/sf/briar/ui/invitation/InvitationParametersImpl.java new file mode 100644 index 000000000..d6933ce56 --- /dev/null +++ b/ui/net/sf/briar/ui/invitation/InvitationParametersImpl.java @@ -0,0 +1,50 @@ +package net.sf.briar.ui.invitation; + +import java.io.File; + +import net.sf.briar.api.i18n.FontManager; +import net.sf.briar.api.invitation.InvitationParameters; + +import com.google.inject.Inject; + +class InvitationParametersImpl implements InvitationParameters { + + private final ExistingUserPanel existingUserPanel; + private final OperatingSystemPanel osPanel; + private final PasswordPanel passwordPanel; + private final LocationPanel locationPanel; + private final FontManager fontManager; + + @Inject + InvitationParametersImpl(ExistingUserPanel existingUserPanel, + OperatingSystemPanel osPanel, PasswordPanel passwordPanel, + LocationPanel locationPanel, FontManager fontManager) { + this.existingUserPanel = existingUserPanel; + this.osPanel = osPanel; + this.passwordPanel = passwordPanel; + this.locationPanel = locationPanel; + this.fontManager = fontManager; + } + + public boolean shouldCreateExe() { + return existingUserPanel.shouldCreateInstaller() + && osPanel.shouldCreateExe(); + } + + public boolean shouldCreateJar() { + return existingUserPanel.shouldCreateInstaller() + && osPanel.shouldCreateJar(); + } + + public char[] getPassword() { + return passwordPanel.getPassword(); + } + + public File getChosenLocation() { + return locationPanel.getChosenDirectory(); + } + + public String[] getBundledFontFilenames() { + return fontManager.getBundledFontFilenames(); + } +} diff --git a/ui/net/sf/briar/ui/invitation/InvitationWizard.java b/ui/net/sf/briar/ui/invitation/InvitationWizard.java new file mode 100644 index 000000000..fcba77cff --- /dev/null +++ b/ui/net/sf/briar/ui/invitation/InvitationWizard.java @@ -0,0 +1,19 @@ +package net.sf.briar.ui.invitation; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.ui.wizard.Wizard; + +class InvitationWizard extends Wizard { + + private static final int WIDTH = 400, HEIGHT = 300; + + InvitationWizard(I18n i18n) { + super(i18n, new Stri18ng("INVITATION_TITLE", i18n), WIDTH, HEIGHT); + } + + public void display() { + showPanel("Intro"); + super.display(); + } +} diff --git a/ui/net/sf/briar/ui/invitation/InvitationWorkerPanel.java b/ui/net/sf/briar/ui/invitation/InvitationWorkerPanel.java new file mode 100644 index 000000000..5451113e5 --- /dev/null +++ b/ui/net/sf/briar/ui/invitation/InvitationWorkerPanel.java @@ -0,0 +1,123 @@ +package net.sf.briar.ui.invitation; + +import java.io.File; +import java.util.List; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.api.invitation.InvitationCallback; +import net.sf.briar.api.invitation.InvitationParameters; +import net.sf.briar.api.invitation.InvitationWorkerFactory; +import net.sf.briar.ui.wizard.Wizard; +import net.sf.briar.ui.wizard.WorkerPanel; +import net.sf.briar.util.StringUtils; + +class InvitationWorkerPanel extends WorkerPanel implements InvitationCallback { + + private static final long serialVersionUID = 3668512976295525240L; + + private static final int MAX_LINE_LENGTH = 40; + + private final InvitationWorkerFactory workerFactory; + private final InvitationParameters parameters; + private final Stri18ng copying, encrypting, created, giveToContact; + private final Stri18ng aborted, error, notFound, notDir, notAllowed; + + InvitationWorkerPanel(Wizard wizard, InvitationWorkerFactory workerFactory, + InvitationParameters parameters, I18n i18n) { + super(wizard, "InvitationWorker", + new Stri18ng("INVITATION_PROGRESS_BEGIN", i18n), + new Stri18ng("CANCELLING", i18n)); + this.workerFactory = workerFactory; + this.parameters = parameters; + copying = new Stri18ng("COPYING_FILE", i18n); + encrypting = new Stri18ng("ENCRYPTING_FILE", i18n); + created = new Stri18ng("INVITATION_CREATED", i18n); + giveToContact = new Stri18ng("INVITATION_GIVE_TO_CONTACT", i18n); + aborted = new Stri18ng("INVITATION_ABORTED", i18n); + error = new Stri18ng("INVITATION_ERROR", i18n); + notFound = new Stri18ng("DIRECTORY_NOT_FOUND", i18n); + notDir = new Stri18ng("FILE_NOT_DIRECTORY", i18n); + notAllowed = new Stri18ng("DIRECTORY_NOT_WRITABLE", i18n); + } + + @Override + protected void backButtonPressed() { + assert false; + } + + @Override + protected void nextButtonPressed() { + assert false; + } + + @Override + protected void finishButtonPressed() { + wizard.close(); + } + + @Override + public void cancelled() { + wizard.close(); + } + + @Override + public void finished() { + wizard.setFinished(true); + } + + @Override + protected Runnable getWorker() { + return workerFactory.createWorker(this, parameters); + } + + public boolean isCancelled() { + return cancelled.get(); + } + + public void copyingFile(File f) { + String path = StringUtils.tail(f.getPath(), MAX_LINE_LENGTH); + String html = copying.html(path); + displayProgress(html); + } + + public void encryptingFile(File f) { + String path = StringUtils.tail(f.getPath(), MAX_LINE_LENGTH); + String html = encrypting.html(path); + displayProgress(html); + } + + public void created(List files) { + StringBuilder s = new StringBuilder(); + for(File f : files) { + if(s.length() > 0) s.append("
"); + s.append(StringUtils.tail(f.getPath(), MAX_LINE_LENGTH)); + } + String filenames = s.toString(); + String html = created.html(filenames, giveToContact.tr()); + done(html); + } + + public void error(String message) { + String html = error.html(message, aborted.tr()); + done(html); + } + + public void notFound(File f) { + String path = StringUtils.tail(f.getPath(), MAX_LINE_LENGTH); + String html = notFound.html(path, aborted.tr()); + done(html); + } + + public void notDirectory(File f) { + String path = StringUtils.tail(f.getPath(), MAX_LINE_LENGTH); + String html = notDir.html(path, aborted.tr()); + done(html); + } + + public void notAllowed(File f) { + String path = StringUtils.tail(f.getPath(), MAX_LINE_LENGTH); + String html = notAllowed.html(path, aborted.tr()); + done(html); + } +} diff --git a/ui/net/sf/briar/ui/invitation/LocationPanel.java b/ui/net/sf/briar/ui/invitation/LocationPanel.java new file mode 100644 index 000000000..b3f52b3df --- /dev/null +++ b/ui/net/sf/briar/ui/invitation/LocationPanel.java @@ -0,0 +1,20 @@ +package net.sf.briar.ui.invitation; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.ui.wizard.DirectoryChooserPanel; +import net.sf.briar.ui.wizard.Wizard; + +import com.google.inject.Inject; + +class LocationPanel extends DirectoryChooserPanel { + + private static final long serialVersionUID = 3788640725729516888L; + + @Inject + LocationPanel(Wizard wizard, I18n i18n) { + super(wizard, "Location", "Password", "InvitationWorker", + new Stri18ng("INVITATION_LOCATION_TITLE", i18n), + new Stri18ng("INVITATION_LOCATION_TEXT", i18n), i18n); + } +} diff --git a/ui/net/sf/briar/ui/invitation/OperatingSystemPanel.java b/ui/net/sf/briar/ui/invitation/OperatingSystemPanel.java new file mode 100644 index 000000000..777bc5b57 --- /dev/null +++ b/ui/net/sf/briar/ui/invitation/OperatingSystemPanel.java @@ -0,0 +1,105 @@ +package net.sf.briar.ui.invitation; + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridLayout; + +import javax.swing.ButtonGroup; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.SwingConstants; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.ui.wizard.Wizard; +import net.sf.briar.ui.wizard.WizardPanel; + +import com.google.inject.Inject; + +class OperatingSystemPanel extends WizardPanel { + + private static final long serialVersionUID = -8370132633634629466L; + + private final Stri18ng question, windows, mac, linux, unknown; + private final JLabel questionLabel; + private final JRadioButton windowsButton, macButton, linuxButton; + private final JRadioButton unknownButton; + + @Inject + OperatingSystemPanel(Wizard wizard, I18n i18n) { + super(wizard, "OperatingSystem"); + question = new Stri18ng("INVITATION_OPERATING_SYSTEM", i18n); + windows = new Stri18ng("WINDOWS", i18n); + mac = new Stri18ng("MAC", i18n); + linux = new Stri18ng("LINUX", i18n); + unknown = new Stri18ng("UNKNOWN", i18n); + questionLabel = new JLabel(question.html()); + Dimension d = wizard.getPreferredSize(); + questionLabel.setPreferredSize(new Dimension(d.width - 50, 50)); + questionLabel.setVerticalAlignment(SwingConstants.TOP); + add(questionLabel); + windowsButton = new JRadioButton(windows.tr()); + macButton = new JRadioButton(mac.tr()); + linuxButton = new JRadioButton(linux.tr()); + unknownButton = new JRadioButton(unknown.tr()); + ButtonGroup group = new ButtonGroup(); + group.add(windowsButton); + group.add(macButton); + group.add(linuxButton); + group.add(unknownButton); + unknownButton.setSelected(true); + JPanel buttonPanel = new JPanel(new GridLayout(4, 1)); + buttonPanel.add(windowsButton); + buttonPanel.add(macButton); + buttonPanel.add(linuxButton); + buttonPanel.add(unknownButton); + add(buttonPanel); + } + + public void localeChanged(Font uiFont) { + questionLabel.setText(question.html()); + questionLabel.setFont(uiFont); + windowsButton.setText(windows.tr()); + windowsButton.setFont(uiFont); + macButton.setText(mac.tr()); + macButton.setFont(uiFont); + linuxButton.setText(linux.tr()); + linuxButton.setFont(uiFont); + } + + @Override + protected void display() { + wizard.setBackButtonEnabled(true); + wizard.setNextButtonEnabled(true); + wizard.setFinished(false); + } + + @Override + protected void backButtonPressed() { + wizard.showPanel("ExistingUser"); + } + + @Override + protected void nextButtonPressed() { + wizard.showPanel("Password"); + } + + @Override + protected void cancelButtonPressed() { + wizard.close(); + } + + @Override + protected void finishButtonPressed() { + assert false; + } + + boolean shouldCreateExe() { + return windowsButton.isSelected() || unknownButton.isSelected(); + } + + boolean shouldCreateJar() { + return !windowsButton.isSelected(); + } +} diff --git a/ui/net/sf/briar/ui/invitation/PasswordPanel.java b/ui/net/sf/briar/ui/invitation/PasswordPanel.java new file mode 100644 index 000000000..343ee0a96 --- /dev/null +++ b/ui/net/sf/briar/ui/invitation/PasswordPanel.java @@ -0,0 +1,135 @@ +package net.sf.briar.ui.invitation; + +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.util.Arrays; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.SwingConstants; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.ui.wizard.Wizard; +import net.sf.briar.ui.wizard.WizardPanel; + +import com.google.inject.Inject; + +public class PasswordPanel extends WizardPanel { + + private static final long serialVersionUID = -1012132977732308293L; + + private final ExistingUserPanel existingUserPanel; + private final Stri18ng intro, enterPassword, confirmPassword; + private final JLabel introLabel, enterPasswordLabel, confirmPasswordLabel; + private final JPasswordField password1, password2; + + @Inject + PasswordPanel(Wizard wizard, ExistingUserPanel existingUserPanel, + I18n i18n) { + super(wizard, "Password"); + this.existingUserPanel = existingUserPanel; + intro = new Stri18ng("INVITATION_PASSWORD", i18n); + enterPassword = new Stri18ng("ENTER_PASSWORD", i18n); + confirmPassword = new Stri18ng("CONFIRM_PASSWORD", i18n); + introLabel = new JLabel(intro.html()); + Dimension d = wizard.getPreferredSize(); + introLabel.setPreferredSize( + new Dimension(d.width - 50, d.height - 140)); + introLabel.setVerticalAlignment(SwingConstants.TOP); + add(introLabel); + JPanel panel1 = new JPanel(new FlowLayout(FlowLayout.LEADING)); + enterPasswordLabel = new JLabel(enterPassword.tr()); + enterPasswordLabel.setPreferredSize( + new Dimension((d.width - 60) / 2, 20)); + password1 = new JPasswordField(); + password1.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent e) { + checkPasswords(); + } + }); + password1.setPreferredSize(new Dimension((d.width - 60) / 2, 20)); + panel1.add(enterPasswordLabel); + panel1.add(password1); + add(panel1); + JPanel panel2 = new JPanel(new FlowLayout(FlowLayout.LEADING)); + confirmPasswordLabel = new JLabel(confirmPassword.tr()); + confirmPasswordLabel.setPreferredSize( + new Dimension((d.width - 60) / 2, 20)); + password2 = new JPasswordField(); + password2.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent e) { + checkPasswords(); + } + }); + password2.setPreferredSize(new Dimension((d.width - 60) / 2, 20)); + panel2.add(confirmPasswordLabel); + panel2.add(password2); + add(panel2); + } + + public void localeChanged(Font uiFont) { + introLabel.setText(intro.html()); + introLabel.setFont(uiFont); + enterPasswordLabel.setText(enterPassword.tr()); + enterPasswordLabel.setFont(uiFont); + confirmPasswordLabel.setText(confirmPassword.tr()); + confirmPasswordLabel.setFont(uiFont); + } + + private void checkPasswords() { + wizard.setNextButtonEnabled(passwordsMatch()); + } + + private boolean passwordsMatch() { + char[] p1 = password1.getPassword(); + char[] p2 = password2.getPassword(); + assert p1 != null && p2 != null; + boolean ok = p1.length > 3 && p2.length > 3 && Arrays.equals(p1, p2); + Arrays.fill(p1, (char) 0); + Arrays.fill(p2, (char) 0); + return ok; + } + + @Override + protected void display() { + wizard.setBackButtonEnabled(true); + wizard.setNextButtonEnabled(false); + wizard.setFinished(false); + password1.setText(""); + password2.setText(""); + } + + @Override + protected void backButtonPressed() { + if(existingUserPanel.shouldCreateInstaller()) + wizard.showPanel("OperatingSystem"); + else wizard.showPanel("ExistingUser"); + } + + @Override + protected void nextButtonPressed() { + wizard.showPanel("Location"); + } + + @Override + protected void cancelButtonPressed() { + wizard.close(); + } + + @Override + protected void finishButtonPressed() { + assert false; + } + + public char[] getPassword() { + if(passwordsMatch()) return password1.getPassword(); + else return null; + } +} diff --git a/ui/net/sf/briar/ui/invitation/UiInvitationModule.java b/ui/net/sf/briar/ui/invitation/UiInvitationModule.java new file mode 100644 index 000000000..742efd4fd --- /dev/null +++ b/ui/net/sf/briar/ui/invitation/UiInvitationModule.java @@ -0,0 +1,32 @@ +package net.sf.briar.ui.invitation; + +import net.sf.briar.api.i18n.FontManager; +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.invitation.InvitationParameters; +import net.sf.briar.api.invitation.InvitationWorkerFactory; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; + +public class UiInvitationModule extends AbstractModule { + + @Override + protected void configure() {} + + @Provides @Singleton + InvitationWizard getInvitationWizard(I18n i18n, FontManager fontManager, + InvitationWorkerFactory workerFactory) { + InvitationWizard wizard = new InvitationWizard(i18n); + new IntroPanel(wizard, i18n); + ExistingUserPanel userPanel = new ExistingUserPanel(wizard, i18n); + OperatingSystemPanel osPanel = new OperatingSystemPanel(wizard, i18n); + PasswordPanel passwordPanel = + new PasswordPanel(wizard, userPanel, i18n); + LocationPanel locationPanel = new LocationPanel(wizard, i18n); + InvitationParameters parameters = new InvitationParametersImpl( + userPanel, osPanel, passwordPanel, locationPanel, fontManager); + new InvitationWorkerPanel(wizard, workerFactory, parameters, i18n); + return wizard; + } +} diff --git a/ui/net/sf/briar/ui/setup/AlreadyInstalledPanel.java b/ui/net/sf/briar/ui/setup/AlreadyInstalledPanel.java new file mode 100644 index 000000000..0ef8b0db5 --- /dev/null +++ b/ui/net/sf/briar/ui/setup/AlreadyInstalledPanel.java @@ -0,0 +1,95 @@ +package net.sf.briar.ui.setup; + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.ButtonGroup; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.SwingConstants; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.ui.wizard.WizardPanel; + +class AlreadyInstalledPanel extends WizardPanel { + + private static final long serialVersionUID = 7908954905165031678L; + + private final Stri18ng question, yes, no; + private final JLabel label; + private final JRadioButton yesButton, noButton; + + AlreadyInstalledPanel(SetupWizard wizard, I18n i18n) { + super(wizard, "AlreadyInstalled"); + question = new Stri18ng("SETUP_ALREADY_INSTALLED", i18n); + yes = new Stri18ng("YES", i18n); + no = new Stri18ng("NO", i18n); + label = new JLabel(question.html()); + Dimension d = wizard.getPreferredSize(); + label.setPreferredSize(new Dimension(d.width - 50, 50)); + label.setVerticalAlignment(SwingConstants.TOP); + add(label); + yesButton = new JRadioButton(yes.tr()); + yesButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + AlreadyInstalledPanel.this.wizard.setNextButtonEnabled(true); + } + }); + noButton = new JRadioButton(no.tr()); + noButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + AlreadyInstalledPanel.this.wizard.setNextButtonEnabled(true); + } + }); + ButtonGroup group = new ButtonGroup(); + group.add(yesButton); + group.add(noButton); + JPanel buttonPanel = new JPanel(new GridLayout(2, 1)); + buttonPanel.add(yesButton); + buttonPanel.add(noButton); + add(buttonPanel); + } + + public void localeChanged(Font uiFont) { + label.setText(question.html()); + label.setFont(uiFont); + yesButton.setText(yes.tr()); + yesButton.setFont(uiFont); + noButton.setText(no.tr()); + noButton.setFont(uiFont); + } + + @Override + protected void display() { + wizard.setBackButtonEnabled(true); + wizard.setNextButtonEnabled(yesButton.isSelected() || noButton.isSelected()); + wizard.setFinished(false); + } + + @Override + protected void backButtonPressed() { + wizard.showPanel("Language"); + } + + @Override + protected void nextButtonPressed() { + if(yesButton.isSelected()) wizard.showPanel("Instructions"); + else if(noButton.isSelected()) wizard.showPanel("Location"); + else assert false; + } + + @Override + protected void cancelButtonPressed() { + wizard.close(); + } + + @Override + protected void finishButtonPressed() { + assert false; + } +} diff --git a/ui/net/sf/briar/ui/setup/InstructionsPanel.java b/ui/net/sf/briar/ui/setup/InstructionsPanel.java new file mode 100644 index 000000000..2963e198d --- /dev/null +++ b/ui/net/sf/briar/ui/setup/InstructionsPanel.java @@ -0,0 +1,41 @@ +package net.sf.briar.ui.setup; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.ui.wizard.TextPanel; + +class InstructionsPanel extends TextPanel { + + private static final long serialVersionUID = -8730283083962607067L; + + InstructionsPanel(SetupWizard wizard, I18n i18n) { + super(wizard, "Instructions", new Stri18ng("SETUP_INSTRUCTIONS", i18n)); + } + + @Override + protected void display() { + wizard.setBackButtonEnabled(true); + wizard.setNextButtonEnabled(false); + wizard.setFinished(true); + } + + @Override + protected void backButtonPressed() { + wizard.showPanel("AlreadyInstalled"); + } + + @Override + protected void nextButtonPressed() { + assert false; + } + + @Override + protected void cancelButtonPressed() { + assert false; + } + + @Override + protected void finishButtonPressed() { + System.exit(0); + } +} diff --git a/ui/net/sf/briar/ui/setup/LanguagePanel.java b/ui/net/sf/briar/ui/setup/LanguagePanel.java new file mode 100644 index 000000000..b139b9c19 --- /dev/null +++ b/ui/net/sf/briar/ui/setup/LanguagePanel.java @@ -0,0 +1,140 @@ +package net.sf.briar.ui.setup; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Locale; + +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; +import javax.swing.SwingConstants; + +import net.sf.briar.api.i18n.FontManager; +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.ui.wizard.WizardPanel; + +class LanguagePanel extends WizardPanel { + + private static final long serialVersionUID = 6692353522360807409L; + + // FIXME: Does this have to be hardcoded? + // Not static because we want the fonts to be loaded first + private final Language english = new Language("English", "en"); + private final Language[] languages = new Language[] { + new Language("\u0627\u0644\u0639\u0631\u0628\u064a\u0629", "ar"), + new Language("\u0f60\u0f51\u0f72\u0f60\u0f72\u0f0b\u0f66\u0f90\u0f7c\u0f62\u0f0d", "bo"), + new Language("\u4e2d\u6587\uff08\u7b80\u4f53\uff09", "cn"), + english, + new Language("\u0641\u0627\u0631\u0633\u06cc", "fa"), + new Language("\u05e2\u05d1\u05e8\u05d9\u05ea", "he"), + new Language("\u65e5\u672c\u8a9e", "ja"), + new Language("\ud55c\uad6d\uc5b4", "ko"), + new Language("\u1006\u102f\u102d\u1010\u1032\u1037", "my"), + new Language("\u0420\u0443\u0441\u0441\u043a\u0438\u0439", "ru"), + new Language("Igpay Atinlay", "pg"), + new Language("\u0e44\u0e17\u0e22", "th"), + new Language("Ti\u1ebfng Vi\u1ec7t", "vi"), + }; + + private final FontManager fontManager; + private final Stri18ng language; + private final JLabel label; + private final JComboBox comboBox; + + LanguagePanel(SetupWizard wizard, FontManager fontManager, + final I18n i18n) { + super(wizard, "Language"); + this.fontManager = fontManager; + language = new Stri18ng("SETUP_LANGUAGE", i18n); + label = new JLabel(language.html()); + Dimension d = wizard.getPreferredSize(); + label.setPreferredSize(new Dimension(d.width - 50, d.height - 120)); + label.setVerticalAlignment(SwingConstants.TOP); + add(label); + comboBox = new JComboBox(); + for(Language l : languages) comboBox.addItem(l); + comboBox.setRenderer(new LanguageRenderer()); + comboBox.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + Language l = (Language) comboBox.getSelectedItem(); + i18n.setLocale(new Locale(l.code)); + } + }); + add(comboBox); + comboBox.setSelectedItem(english); + } + + public void localeChanged(Font uiFont) { + label.setText(language.html()); + label.setFont(uiFont); + comboBox.setFont(uiFont); + } + + @Override + protected void display() { + wizard.setBackButtonEnabled(false); + wizard.setNextButtonEnabled(true); + wizard.setFinished(false); + } + + @Override + protected void backButtonPressed() { + assert false; + } + + @Override + protected void nextButtonPressed() { + wizard.showPanel("AlreadyInstalled"); + } + + @Override + protected void cancelButtonPressed() { + System.exit(0); + } + + @Override + protected void finishButtonPressed() { + assert false; + } + + private static class Language { + + private final String name, code; + + Language(String name, String code) { + this.name = name; + this.code = code; + } + } + + private class LanguageRenderer extends JLabel implements ListCellRenderer { + + private static final long serialVersionUID = 8562749521807769004L; + + LanguageRenderer() { + setHorizontalAlignment(SwingConstants.CENTER); + setVerticalAlignment(SwingConstants.CENTER); + setPreferredSize(new Dimension(100, 20)); + } + + public Component getListCellRendererComponent(JList list, Object value, + int index, boolean isSelected, boolean cellHasFocus) { + Language language = (Language) value; + setText(language.name); + setFont(fontManager.getFontForLanguage(language.code)); + if(isSelected) { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + } else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + } + return this; + } + } +} diff --git a/ui/net/sf/briar/ui/setup/LocationPanel.java b/ui/net/sf/briar/ui/setup/LocationPanel.java new file mode 100644 index 000000000..e73170a5e --- /dev/null +++ b/ui/net/sf/briar/ui/setup/LocationPanel.java @@ -0,0 +1,16 @@ +package net.sf.briar.ui.setup; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.ui.wizard.DirectoryChooserPanel; + +public class LocationPanel extends DirectoryChooserPanel { + + private static final long serialVersionUID = -8831098591612528860L; + + LocationPanel(SetupWizard wizard, I18n i18n) { + super(wizard, "Location", "AlreadyInstalled", "SetupWorker", + new Stri18ng("SETUP_LOCATION_TITLE", i18n), + new Stri18ng("SETUP_LOCATION_TEXT", i18n), i18n); + } +} diff --git a/ui/net/sf/briar/ui/setup/SetupParametersImpl.java b/ui/net/sf/briar/ui/setup/SetupParametersImpl.java new file mode 100644 index 000000000..1aca3ec08 --- /dev/null +++ b/ui/net/sf/briar/ui/setup/SetupParametersImpl.java @@ -0,0 +1,25 @@ +package net.sf.briar.ui.setup; + +import java.io.File; + +import net.sf.briar.api.i18n.FontManager; +import net.sf.briar.api.setup.SetupParameters; + +class SetupParametersImpl implements SetupParameters { + + private final LocationPanel locationPanel; + private final FontManager fontManager; + + SetupParametersImpl(LocationPanel locationPanel, FontManager fontManager) { + this.locationPanel = locationPanel; + this.fontManager = fontManager; + } + + public File getChosenLocation() { + return locationPanel.getChosenDirectory(); + } + + public String[] getBundledFontFilenames() { + return fontManager.getBundledFontFilenames(); + } +} diff --git a/ui/net/sf/briar/ui/setup/SetupWizard.java b/ui/net/sf/briar/ui/setup/SetupWizard.java new file mode 100644 index 000000000..015315780 --- /dev/null +++ b/ui/net/sf/briar/ui/setup/SetupWizard.java @@ -0,0 +1,19 @@ +package net.sf.briar.ui.setup; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.ui.wizard.Wizard; + +public class SetupWizard extends Wizard { + + private static int WIDTH = 400, HEIGHT = 300; + + SetupWizard(I18n i18n) { + super(i18n, new Stri18ng("SETUP_TITLE", i18n), WIDTH, HEIGHT); + } + + public void display() { + showPanel("Language"); + super.display(); + } +} \ No newline at end of file diff --git a/ui/net/sf/briar/ui/setup/SetupWorkerPanel.java b/ui/net/sf/briar/ui/setup/SetupWorkerPanel.java new file mode 100644 index 000000000..da979b7e0 --- /dev/null +++ b/ui/net/sf/briar/ui/setup/SetupWorkerPanel.java @@ -0,0 +1,116 @@ +package net.sf.briar.ui.setup; + +import java.io.File; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; +import net.sf.briar.api.setup.SetupCallback; +import net.sf.briar.api.setup.SetupParameters; +import net.sf.briar.api.setup.SetupWorkerFactory; +import net.sf.briar.ui.wizard.WorkerPanel; +import net.sf.briar.util.StringUtils; + +class SetupWorkerPanel extends WorkerPanel implements SetupCallback { + + private static final long serialVersionUID = 6596714579098160155L; + + private static final int MAX_LINE_LENGTH = 40; + + private final SetupWorkerFactory workerFactory; + private final SetupParameters parameters; + private final Stri18ng extracting, copying, installed, uninstall; + private final Stri18ng aborted, error, notFound, notDir, notAllowed; + + SetupWorkerPanel(SetupWizard wizard, SetupWorkerFactory workerFactory, + SetupParameters parameters, I18n i18n) { + super(wizard, "SetupWorker", + new Stri18ng("SETUP_PROGRESS_BEGIN", i18n), + new Stri18ng("CANCELLING", i18n)); + this.workerFactory = workerFactory; + this.parameters = parameters; + extracting = new Stri18ng("EXTRACTING_FILE", i18n); + copying = new Stri18ng("COPYING_FILE", i18n); + installed = new Stri18ng("SETUP_INSTALLED", i18n); + uninstall = new Stri18ng("SETUP_UNINSTALL", i18n); + aborted = new Stri18ng("SETUP_ABORTED", i18n); + error = new Stri18ng("SETUP_ERROR", i18n); + notFound = new Stri18ng("DIRECTORY_NOT_FOUND", i18n); + notDir = new Stri18ng("FILE_NOT_DIRECTORY", i18n); + notAllowed = new Stri18ng("DIRECTORY_NOT_WRITABLE", i18n); + } + + @Override + protected void backButtonPressed() { + assert false; + } + + @Override + protected void nextButtonPressed() { + assert false; + } + + @Override + protected void finishButtonPressed() { + System.exit(0); + } + + @Override + public void cancelled() { + System.exit(0); + } + + @Override + public void finished() { + wizard.setFinished(true); + } + + @Override + protected Runnable getWorker() { + return workerFactory.createWorker(this, parameters); + } + + public boolean isCancelled() { + return cancelled.get(); + } + + public void extractingFile(File f) { + String path = StringUtils.tail(f.getPath(), MAX_LINE_LENGTH); + String html = extracting.html(path); + displayProgress(html); + } + + public void copyingFile(File f) { + String path = StringUtils.tail(f.getPath(), MAX_LINE_LENGTH); + String html = copying.html(path); + displayProgress(html); + } + + public void installed(File f) { + String path = StringUtils.tail(f.getPath(), MAX_LINE_LENGTH); + String html = installed.html(path, uninstall.tr()); + done(html); + } + + public void error(String message) { + String html = error.html(message, aborted.tr()); + done(html); + } + + public void notFound(File f) { + String path = StringUtils.tail(f.getPath(), MAX_LINE_LENGTH); + String html = notFound.html(path, aborted.tr()); + done(html); + } + + public void notDirectory(File f) { + String path = StringUtils.tail(f.getPath(), MAX_LINE_LENGTH); + String html = notDir.html(path, aborted.tr()); + done(html); + } + + public void notAllowed(File f) { + String path = StringUtils.tail(f.getPath(), MAX_LINE_LENGTH); + String html = notAllowed.html(path, aborted.tr()); + done(html); + } +} diff --git a/ui/net/sf/briar/ui/setup/UiSetupModule.java b/ui/net/sf/briar/ui/setup/UiSetupModule.java new file mode 100644 index 000000000..26b8df46d --- /dev/null +++ b/ui/net/sf/briar/ui/setup/UiSetupModule.java @@ -0,0 +1,30 @@ +package net.sf.briar.ui.setup; + +import net.sf.briar.api.i18n.FontManager; +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.setup.SetupParameters; +import net.sf.briar.api.setup.SetupWorkerFactory; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; + +public class UiSetupModule extends AbstractModule { + + @Override + protected void configure() {} + + @Provides @Singleton + SetupWizard getSetupWizard(I18n i18n, FontManager fontManager, + SetupWorkerFactory workerFactory) { + SetupWizard wizard = new SetupWizard(i18n); + new LanguagePanel(wizard, fontManager, i18n); + new AlreadyInstalledPanel(wizard, i18n); + new InstructionsPanel(wizard, i18n); + LocationPanel locationPanel = new LocationPanel(wizard, i18n); + SetupParameters parameters = + new SetupParametersImpl(locationPanel, fontManager); + new SetupWorkerPanel(wizard, workerFactory, parameters, i18n); + return wizard; + } +} diff --git a/ui/net/sf/briar/ui/wizard/DirectoryChooserPanel.java b/ui/net/sf/briar/ui/wizard/DirectoryChooserPanel.java new file mode 100644 index 000000000..5fbe0f732 --- /dev/null +++ b/ui/net/sf/briar/ui/wizard/DirectoryChooserPanel.java @@ -0,0 +1,73 @@ +package net.sf.briar.ui.wizard; + +import java.io.File; + +import javax.swing.JFileChooser; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; + +public class DirectoryChooserPanel extends TextPanel { + + private static final long serialVersionUID = 6692353522360807409L; + + private final String prevId, nextId; + private final Stri18ng title; + private final I18n i18n; + private volatile File chosenDirectory = null; + + protected DirectoryChooserPanel(Wizard wizard, String id, String prevId, + String nextId, Stri18ng title, Stri18ng text, I18n i18n) { + super(wizard, id, text); + this.prevId = prevId; + this.nextId = nextId; + this.title = title; + this.i18n = i18n; + } + + @Override + protected void display() { + wizard.setBackButtonEnabled(true); + wizard.setNextButtonEnabled(true); + wizard.setFinished(false); + } + + @Override + protected void backButtonPressed() { + wizard.showPanel(prevId); + } + + @Override + protected void nextButtonPressed() { + JFileChooser chooser; + String home = System.getProperty("user.home"); + if(home == null) chooser = new JFileChooser(); + else chooser = new JFileChooser(home); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setDialogTitle(title.tr()); + chooser.setComponentOrientation(i18n.getComponentOrientation()); + int result = chooser.showSaveDialog(this); + if(result == JFileChooser.APPROVE_OPTION) { + File dir = chooser.getSelectedFile(); + assert dir != null; + assert dir.exists(); + assert dir.isDirectory(); + chosenDirectory = dir; + wizard.showPanel(nextId); + } + } + + @Override + protected void cancelButtonPressed() { + wizard.close(); + } + + @Override + protected void finishButtonPressed() { + assert false; + } + + public File getChosenDirectory() { + return chosenDirectory; + } +} diff --git a/ui/net/sf/briar/ui/wizard/TextPanel.java b/ui/net/sf/briar/ui/wizard/TextPanel.java new file mode 100644 index 000000000..272b193da --- /dev/null +++ b/ui/net/sf/briar/ui/wizard/TextPanel.java @@ -0,0 +1,32 @@ +package net.sf.briar.ui.wizard; + +import java.awt.Dimension; +import java.awt.Font; + +import javax.swing.JLabel; +import javax.swing.SwingConstants; + +import net.sf.briar.api.i18n.Stri18ng; + +public abstract class TextPanel extends WizardPanel { + + private static final long serialVersionUID = -3046102503813671049L; + + private final Stri18ng text; + private final JLabel label; + + protected TextPanel(Wizard wizard, String id, Stri18ng text) { + super(wizard, id); + this.text = text; + label = new JLabel(text.html()); + Dimension d = wizard.getPreferredSize(); + label.setPreferredSize(new Dimension(d.width - 50, d.height - 80)); + label.setVerticalAlignment(SwingConstants.TOP); + add(label); + } + + public void localeChanged(Font uiFont) { + label.setText(text.html()); + label.setFont(uiFont); + } +} diff --git a/ui/net/sf/briar/ui/wizard/Wizard.java b/ui/net/sf/briar/ui/wizard/Wizard.java new file mode 100644 index 000000000..1dace0909 --- /dev/null +++ b/ui/net/sf/briar/ui/wizard/Wizard.java @@ -0,0 +1,179 @@ +package net.sf.briar.ui.wizard; + +import java.awt.BorderLayout; +import java.awt.CardLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; + +import net.sf.briar.api.i18n.I18n; +import net.sf.briar.api.i18n.Stri18ng; + +public class Wizard implements I18n.Listener { + + private final I18n i18n; + private final Stri18ng title, back, next, cancel, finish; + private final Map panels; + private final JPanel cardPanel; + private final CardLayout cardLayout; + private final JButton backButton, nextButton, cancelButton; + private final JFrame frame; + private final Object finishedLock = new Object(); + private WizardPanel currentPanel = null; + private volatile boolean finished = false; + + public Wizard(I18n i18n, Stri18ng title, int width, int height) { + this.i18n = i18n; + this.title = title; + back = new Stri18ng("BACK", i18n); + next = new Stri18ng("NEXT", i18n); + cancel = new Stri18ng("CANCEL", i18n); + finish = new Stri18ng("FINISH", i18n); + panels = new HashMap(); + cardPanel = new JPanel(); + cardPanel.setBorder(new EmptyBorder(new Insets(5, 10, 5, 10))); + cardLayout = new CardLayout(); + cardPanel.setLayout(cardLayout); + + backButton = new JButton(back.tr()); + backButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + backButtonPressed(); + } + }); + nextButton = new JButton(next.tr()); + nextButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + nextButtonPressed(); + } + }); + cancelButton = new JButton(cancel.tr()); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + closeButtonPressed(); + } + }); + + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.TRAILING)); + buttonPanel.setBorder(new EmptyBorder(new Insets(5, 10, 5, 10))); + buttonPanel.add(backButton); + buttonPanel.add(Box.createHorizontalStrut(10)); + buttonPanel.add(nextButton); + buttonPanel.add(Box.createHorizontalStrut(30)); + buttonPanel.add(cancelButton); + + frame = new JFrame(title.tr()); + frame.setPreferredSize(new Dimension(width, height)); + frame.setResizable(false); + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + closeButtonPressed(); + } + }); + frame.getContentPane().add(cardPanel, BorderLayout.CENTER); + frame.getContentPane().add(buttonPanel, BorderLayout.SOUTH); + } + + public void localeChanged(Font uiFont) { + backButton.setText(back.tr()); + backButton.setFont(uiFont); + nextButton.setText(next.tr()); + nextButton.setFont(uiFont); + synchronized(finishedLock) { + if(finished) cancelButton.setText(finish.tr()); + else cancelButton.setText(cancel.tr()); + } + cancelButton.setFont(uiFont); + frame.setTitle(title.tr()); + for(WizardPanel panel : panels.values()) panel.localeChanged(uiFont); + frame.applyComponentOrientation(i18n.getComponentOrientation()); + SwingUtilities.updateComponentTreeUI(frame); + } + + public void display() { + assert currentPanel != null; + i18n.addListener(this); + frame.pack(); + frame.setLocationRelativeTo(null); // Centre of the screen + frame.setVisible(true); + } + + public void close() { + i18n.removeListener(this); + frame.setVisible(false); + frame.dispose(); + } + + public void registerPanel(String id, WizardPanel panel) { + assert currentPanel == null; + WizardPanel old = panels.put(id, panel); + assert old == null; + cardPanel.add(id, panel); + } + + public void showPanel(String id) { + currentPanel = panels.get(id); + assert currentPanel != null; + cardLayout.show(cardPanel, id); + currentPanel.display(); + } + + public void setBackButtonEnabled(boolean enabled) { + backButton.setEnabled(enabled); + } + + public void setNextButtonEnabled(boolean enabled) { + nextButton.setEnabled(enabled); + } + + public void setFinished(boolean finished) { + synchronized(finishedLock) { + this.finished = finished; + if(finished) { + nextButton.setEnabled(false); + cancelButton.setText(finish.tr()); + } else cancelButton.setText(cancel.tr()); + } + } + + public Dimension getPreferredSize() { + return frame.getPreferredSize(); + } + + private void backButtonPressed() { + assert SwingUtilities.isEventDispatchThread(); + assert currentPanel != null; + currentPanel.backButtonPressed(); + } + + private void nextButtonPressed() { + assert SwingUtilities.isEventDispatchThread(); + assert currentPanel != null; + currentPanel.nextButtonPressed(); + } + + private void closeButtonPressed() { + assert SwingUtilities.isEventDispatchThread(); + assert currentPanel != null; + cancelButton.setEnabled(false); + synchronized(finishedLock) { + if(finished) currentPanel.finishButtonPressed(); + else currentPanel.cancelButtonPressed(); + } + } +} \ No newline at end of file diff --git a/ui/net/sf/briar/ui/wizard/WizardPanel.java b/ui/net/sf/briar/ui/wizard/WizardPanel.java new file mode 100644 index 000000000..42116e6b7 --- /dev/null +++ b/ui/net/sf/briar/ui/wizard/WizardPanel.java @@ -0,0 +1,27 @@ +package net.sf.briar.ui.wizard; + +import javax.swing.JPanel; + +import net.sf.briar.api.i18n.I18n; + +public abstract class WizardPanel extends JPanel implements I18n.Listener { + + private static final long serialVersionUID = 8657047449339969485L; + + protected final Wizard wizard; + + protected WizardPanel(Wizard wizard, String id) { + this.wizard = wizard; + wizard.registerPanel(id, this); + } + + protected abstract void display(); + + protected abstract void backButtonPressed(); + + protected abstract void nextButtonPressed(); + + protected abstract void cancelButtonPressed(); + + protected abstract void finishButtonPressed(); +} diff --git a/ui/net/sf/briar/ui/wizard/WorkerPanel.java b/ui/net/sf/briar/ui/wizard/WorkerPanel.java new file mode 100644 index 000000000..a003281cc --- /dev/null +++ b/ui/net/sf/briar/ui/wizard/WorkerPanel.java @@ -0,0 +1,90 @@ +package net.sf.briar.ui.wizard; + +import java.awt.Dimension; +import java.awt.Font; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.swing.JLabel; +import javax.swing.JProgressBar; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; + +import net.sf.briar.api.i18n.Stri18ng; + +public abstract class WorkerPanel extends WizardPanel { + + private static final long serialVersionUID = -3761407066345183330L; + + private final Stri18ng starting, cancelling; + private final JLabel label; + private final JProgressBar progress; + private final AtomicBoolean started; + + protected final AtomicBoolean cancelled; + + protected WorkerPanel(Wizard wizard, String id, Stri18ng starting, + Stri18ng cancelling) { + super(wizard, id); + this.starting = starting; + this.cancelling = cancelling; + label = new JLabel(starting.html()); + Dimension d = wizard.getPreferredSize(); + label.setPreferredSize(new Dimension(d.width - 50, d.height - 120)); + label.setVerticalAlignment(SwingConstants.TOP); + add(label); + progress = new JProgressBar(); + progress.setIndeterminate(true); + progress.setPreferredSize(new Dimension(d.width - 50, 20)); + add(progress); + started = new AtomicBoolean(false); + cancelled = new AtomicBoolean(false); + } + + public void localeChanged(Font uiFont) { + label.setText(starting.html()); + label.setFont(uiFont); + } + + public abstract void cancelled(); + + public abstract void finished(); + + protected abstract Runnable getWorker(); + + @Override + protected void display() { + if(!started.getAndSet(true)) { + wizard.setBackButtonEnabled(false); + wizard.setNextButtonEnabled(false); + wizard.setFinished(false); + new Thread(getWorker()).start(); + } + } + + @Override + protected void cancelButtonPressed() { + if(!cancelled.getAndSet(true)) { + wizard.setBackButtonEnabled(false); + wizard.setNextButtonEnabled(false); + label.setText(cancelling.html()); + } + } + + public void displayProgress(final String message) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + label.setText("" + message + ""); + } + }); + } + + public void done(final String message) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setVisible(false); + label.setText("" + message + ""); + finished(); + } + }); + } +} diff --git a/unzip60.patch b/unzip60.patch new file mode 100644 index 000000000..4873a0091 --- /dev/null +++ b/unzip60.patch @@ -0,0 +1,98 @@ +diff -Bbur unzip60/process.c unzip60-briar/process.c +--- unzip60/process.c 2009-03-06 01:25:10.000000000 +0000 ++++ unzip60-briar/process.c 2011-04-18 16:03:46.000000000 +0100 +@@ -370,15 +370,8 @@ + G.zipfn)); + } + #ifdef CHEAP_SFX_AUTORUN +- if (G.autorun_command[0] && !uO.qflag) { /* NO autorun without prompt! */ +- Info(slide, 0x81, ((char *)slide, LoadFarString(AutorunPrompt), +- FnFilter1(G.autorun_command))); +- if (fgets(G.answerbuf, 9, stdin) != (char *)NULL +- && toupper(*G.answerbuf) == 'Y') ++ /* MJR 2011-04-18: Allow autorun without prompt. */ + system(G.autorun_command); +- else +- Info(slide, 1, ((char *)slide, LoadFarString(NotAutoRunning))); +- } + #endif /* CHEAP_SFX_AUTORUN */ + + #else /* !SFX */ +diff -Bbur unzip60/unzip.c unzip60-briar/unzip.c +--- unzip60/unzip.c 2009-04-16 19:26:52.000000000 +0100 ++++ unzip60-briar/unzip.c 2011-06-18 11:23:26.000000000 +0100 +@@ -358,7 +358,8 @@ + # else + static ZCONST char Far UnzipSFXBanner[] = + # endif +- "UnZipSFX %d.%d%d%s of %s, by Info-ZIP (http://www.info-zip.org).\n"; ++ /* MJR 2011-04-18: Modified banner as required by license. */ ++ "This self-extractor is based on UnZipSFX by Info-ZIP.\nThis is NOT an official release and it is NOT supported by Info-ZIP.\n"; + # ifdef SFX_EXDIR + static ZCONST char Far UnzipSFXOpts[] = + "Valid options are -tfupcz and -d ; modifiers are -abjnoqCL%sV%s.\n"; +@@ -1236,6 +1237,11 @@ + Info(slide, 0x401, ((char *)slide, LoadFarString(NotExtracting))); + #endif /* ?(SFX && !SFX_EXDIR) */ + ++#ifdef SFX ++ /* MJR 2011-06-18: Self-extract to this directory. This is a hack. */ ++ uO.exdir = "briar.tmp"; ++#endif ++ + #ifdef UNICODE_SUPPORT + /* set Unicode-escape-all if option -U used */ + if (uO.U_flag == 1) +@@ -1903,10 +1909,9 @@ + + #ifdef SFX + /* print our banner unless we're being fairly quiet */ ++ /* MJR 2011-04-18: Removed unused arguments from modified banner. */ + if (uO.qflag < 2) +- Info(slide, error? 1 : 0, ((char *)slide, LoadFarString(UnzipSFXBanner), +- UZ_MAJORVER, UZ_MINORVER, UZ_PATCHLEVEL, UZ_BETALEVEL, +- LoadFarStringSmall(VersionDate))); ++ Info(slide, error? 1 : 0, ((char *)slide, LoadFarString(UnzipSFXBanner))); + #ifdef BETA + /* always print the beta warning: no unauthorized distribution!! */ + Info(slide, error? 1 : 0, ((char *)slide, LoadFarString(BetaVersion), "\n", +@@ -1925,6 +1930,12 @@ + + *pargc = argc; + *pargv = argv; ++ ++#ifdef SFX ++ /* MJR 2011-04-18: SFX should always overwrite without prompting */ ++ uO.overwrite_all = 2; ++#endif ++ + return PK_OK; + + } /* end function uz_opts() */ +@@ -1976,9 +1987,8 @@ + __GDEF + int error; + { +- Info(slide, error? 1 : 0, ((char *)slide, LoadFarString(UnzipSFXBanner), +- UZ_MAJORVER, UZ_MINORVER, UZ_PATCHLEVEL, UZ_BETALEVEL, +- LoadFarStringSmall(VersionDate))); ++ /* MJR 2011-04-18: Removed unused arguments from modified banner. */ ++ Info(slide, error? 1 : 0, ((char *)slide, LoadFarString(UnzipSFXBanner))); + Info(slide, error? 1 : 0, ((char *)slide, LoadFarString(UnzipSFXOpts), + SFXOPT1, LOCAL)); + #ifdef BETA +diff -Bbur unzip60/win32/Makefile.gcc unzip60-briar/win32/Makefile.gcc +--- unzip60/win32/Makefile.gcc 2008-08-09 17:03:30.000000000 +0100 ++++ unzip60-briar/win32/Makefile.gcc 2011-06-18 11:18:38.000000000 +0100 +@@ -262,8 +262,10 @@ + unzip$(EXE): $(OBJU) $(LIBBZIP2) + $(LD) $(LDFLAGS) $(LDVER) $(OBJU) $(LD_BZ2LIB) $(LDLIBS) + ++# MJR 2011-06-18: Added -mwindows flag to suppress terminal window. ++ + unzipsfx$(EXE): $(OBJX) $(LIBBZIP2X) +- $(LD) $(LDFLAGS) $(LDVER) $(OBJX) $(LDLIBS) ++ $(LD) $(LDFLAGS) $(LDVER) $(OBJX) $(LDLIBS) -mwindows + + funzip$(EXE): $(OBJF) + $(LD) $(LDFLAGS) $(LDVER) $(OBJF) $(LDLIBS) diff --git a/util/.gitignore b/util/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/util/.gitignore @@ -0,0 +1 @@ +/build diff --git a/util/build.xml b/util/build.xml new file mode 100644 index 000000000..87f79eb6a --- /dev/null +++ b/util/build.xml @@ -0,0 +1,3 @@ + + + diff --git a/util/net/sf/briar/util/FileUtils.java b/util/net/sf/briar/util/FileUtils.java new file mode 100644 index 000000000..887c8d7d9 --- /dev/null +++ b/util/net/sf/briar/util/FileUtils.java @@ -0,0 +1,90 @@ +package net.sf.briar.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.CodeSource; + +public class FileUtils { + + /** + * Returns the directory where Briar is installed. + */ + public static File getBriarDirectory() { + CodeSource c = FileUtils.class.getProtectionDomain().getCodeSource(); + File f = new File(c.getLocation().getPath()); + assert f.exists(); + if(f.isFile()) { + // Running from a jar - return the jar's grandparent + try { + f = f.getCanonicalFile().getParentFile().getParentFile(); + } catch(IOException e) { + throw new RuntimeException(e); + } + } else { + // Running from Eclipse + try { + f = new File(f.getCanonicalFile().getParentFile(), "Briar"); + } catch(IOException e) { + throw new RuntimeException(e); + } + f.mkdir(); + } + assert f.exists(); + assert f.isDirectory(); + return f; + } + + /** + * Creates and returns a temporary file. + */ + public static File createTempFile() throws IOException { + String rand = String.valueOf(1000 + (int) (Math.random() * 9000)); + return File.createTempFile(rand, null); + } + + /** + * Copies the contents of the source file to the destination file. + */ + public static void copy(File src, File dest) throws IOException { + FileInputStream in = new FileInputStream(src); + copy(in, dest); + } + + /** + * Copies the contents of the input stream to the destination file. + */ + public static void copy(InputStream in, File dest) throws IOException { + FileOutputStream out = new FileOutputStream(dest); + byte[] buf = new byte[1024]; + int i; + while((i = in.read(buf, 0, buf.length)) != -1) out.write(buf, 0, i); + in.close(); + out.flush(); + out.close(); + } + + /** + * Copies the source file or directory to the destination directory. + */ + public static void copyRecursively(File src, File dest, Callback callback) + throws IOException { + assert dest.exists(); + assert dest.isDirectory(); + dest = new File(dest, src.getName()); + if(src.isDirectory()) { + dest.mkdir(); + for(File f : src.listFiles()) copyRecursively(f, dest, callback); + } else { + if(callback != null) callback.processingFile(dest); + copy(src, dest); + } + } + + public interface Callback { + + void processingFile(File f); + } +} diff --git a/util/net/sf/briar/util/OsUtils.java b/util/net/sf/briar/util/OsUtils.java new file mode 100644 index 000000000..f8e8553da --- /dev/null +++ b/util/net/sf/briar/util/OsUtils.java @@ -0,0 +1,18 @@ +package net.sf.briar.util; + +public class OsUtils { + + private static final String os = System.getProperty("os.name"); + + public static boolean isWindows() { + return os.indexOf("Windows") != -1; + } + + public static boolean isMac() { + return os.indexOf("Mac OS") != -1; + } + + public static boolean isLinux() { + return os.indexOf("Linux") != -1; + } +} diff --git a/util/net/sf/briar/util/StringUtils.java b/util/net/sf/briar/util/StringUtils.java new file mode 100644 index 000000000..bcd9947c3 --- /dev/null +++ b/util/net/sf/briar/util/StringUtils.java @@ -0,0 +1,14 @@ +package net.sf.briar.util; + +public class StringUtils { + + public static String head(String s, int length) { + if(s.length() > length) return s.substring(0, length) + "..."; + else return s; + } + + public static String tail(String s, int length) { + if(s.length() > length) return "..." + s.substring(s.length() - length); + else return s; + } +} diff --git a/util/net/sf/briar/util/ZipUtils.java b/util/net/sf/briar/util/ZipUtils.java new file mode 100644 index 000000000..cc5aeacd9 --- /dev/null +++ b/util/net/sf/briar/util/ZipUtils.java @@ -0,0 +1,78 @@ +package net.sf.briar.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public class ZipUtils { + + public static void copyToZip(String path, File file, ZipOutputStream zip) + throws IOException { + assert file.isFile() : file.getAbsolutePath(); + zip.putNextEntry(new ZipEntry(path)); + FileInputStream in = new FileInputStream(file); + byte[] buf = new byte[1024]; + int i; + while((i = in.read(buf, 0, buf.length)) != -1) zip.write(buf, 0, i); + in.close(); + zip.closeEntry(); + } + + public static void copyToZipRecursively(String path, File dir, + ZipOutputStream zip, Callback callback) throws IOException { + assert dir.isDirectory(); + for(File child : dir.listFiles()) { + String childPath = extendPath(path, child.getName()); + if(child.isDirectory()) { + copyToZipRecursively(childPath, child, zip, callback); + } else { + if(callback != null) callback.processingFile(child); + copyToZip(childPath, child, zip); + } + } + } + + private static String extendPath(String path, String name) { + if(path == null || path.equals("")) return name; + else return path + "/" + name; + } + + public static void unzipStream(InputStream in, File dir, String regex, + Callback callback) throws IOException { + String path = dir.getCanonicalPath(); + ZipInputStream zip = new ZipInputStream(in); + byte[] buf = new byte[1024]; + ZipEntry entry; + while((entry = zip.getNextEntry()) != null) { + String name = entry.getName(); + if(name.matches(regex)) { + File file = new File(path + "/" + name); + if(callback != null) callback.processingFile(file); + if(entry.isDirectory()) { + file.mkdirs(); + } else { + file.getParentFile().mkdirs(); + FileOutputStream out = new FileOutputStream(file); + int i; + while((i = zip.read(buf, 0, buf.length)) > 0) { + out.write(buf, 0, i); + } + out.flush(); + out.close(); + } + } + zip.closeEntry(); + } + zip.close(); + } + + public interface Callback { + + void processingFile(File f); + } +} \ No newline at end of file