blob: 99588f1be30738860a1ccf3daaed25348c063b58 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.syncslidepresenter;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Uninterruptibles;
import org.joda.time.Duration;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Window;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.WindowConstants;
import io.v.android.apps.syncslides.db.VCurrentSlide;
import io.v.android.apps.syncslides.db.VSlide;
import io.v.android.apps.syncslides.discovery.ParticipantClient;
import io.v.android.apps.syncslides.discovery.ParticipantClientFactory;
import io.v.android.apps.syncslides.discovery.Presentation;
import io.v.impl.google.naming.NamingUtil;
import io.v.impl.google.services.syncbase.SyncbaseServer;
import io.v.v23.V;
import io.v.v23.VIterable;
import io.v.v23.context.VContext;
import io.v.v23.namespace.Namespace;
import io.v.v23.naming.Endpoint;
import io.v.v23.naming.GlobReply;
import io.v.v23.naming.MountEntry;
import io.v.v23.naming.MountedServer;
import io.v.v23.rpc.Server;
import io.v.v23.security.BlessingPattern;
import io.v.v23.security.access.AccessList;
import io.v.v23.security.access.Permissions;
import io.v.v23.services.syncbase.nosql.KeyValue;
import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
import io.v.v23.services.watch.ResumeMarker;
import io.v.v23.syncbase.Syncbase;
import io.v.v23.syncbase.SyncbaseApp;
import io.v.v23.syncbase.SyncbaseService;
import io.v.v23.syncbase.nosql.BatchDatabase;
import io.v.v23.syncbase.nosql.Database;
import io.v.v23.syncbase.nosql.RowRange;
import io.v.v23.syncbase.nosql.Syncgroup;
import io.v.v23.syncbase.nosql.Table;
import io.v.v23.syncbase.nosql.WatchChange;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;
/**
* The entry point for syncslidepresenter. To run:
*
* <pre>
* cd $JIRI_ROOT/release/java
* ./gradlew :projects:syncslidepresenter:installDist
* ./projects/syncslidepresenter/build/install/syncslidepresenter/bin/syncslidepresenter
* </pre>
*/
public class Main {
private static final Logger logger = Logger.getLogger(Main.class.getName());
private static final String SYNCBASE_APP = "syncslides";
private static final String SYNCBASE_DB = "syncslides";
private static final String PRESENTATIONS_TABLE = "Presentations";
private static final String DECKS_TABLE = "Decks";
private final Table presentations;
private final Table decks;
private final ImageViewer viewer;
private Database db;
private VContext context;
public static void main(String[] args) throws SyncbaseServer.StartException, VException, IOException {
Options options = new Options();
JCommander commander = new JCommander(options);
try {
commander.parse(args);
} catch (ParameterException e) {
logger.warning("Could not parse parameters: " + e.getMessage());
commander.usage();
return;
}
if (options.help) {
commander.usage();
return;
}
// Make command-Q do the same as closing the main frame (i.e. exit).
System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
JFrame frame = new JFrame();
enableOSXFullscreen(frame);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setVisible(true);
VContext baseContext = V.init();
AccessList acl = new AccessList(ImmutableList.of(new BlessingPattern("...")),
ImmutableList.<String>of());
Permissions permissions = new Permissions(ImmutableMap.of("1", acl));
String name = NamingUtil.join(options.mountPrefix, UUID.randomUUID().toString());
logger.info("Mounting new syncbase server at " + name);
VContext mountContext = SyncbaseServer.withNewServer(baseContext,
new SyncbaseServer.Params().withPermissions(permissions).withName(name)
.withStorageRootDir(options.storageRootDir));
final Server server = V.getServer(mountContext);
if (server.getStatus().getEndpoints().length > 0) {
logger.info("Mounted syncbase server at the following endpoints: ");
for (Endpoint e : server.getStatus().getEndpoints()) {
logger.info("\t" + e);
}
logger.info("End of endpoint list");
SyncbaseService service
= Syncbase.newService("/" + server.getStatus().getEndpoints()[0]);
SyncbaseApp app = service.getApp(SYNCBASE_APP);
if (!app.exists(baseContext)) {
app.create(baseContext, permissions);
}
Database db = app.getNoSqlDatabase(SYNCBASE_DB, null);
if (!db.exists(baseContext)) {
db.create(baseContext, permissions);
}
Table decks = db.getTable(DECKS_TABLE);
if (!decks.exists(baseContext)) {
decks.create(baseContext, permissions);
}
Table presentations = db.getTable(PRESENTATIONS_TABLE);
if (!presentations.exists(baseContext)) {
presentations.create(baseContext, permissions);
}
JPanel panel = new JPanel(new GridBagLayout());
ScaleToFitJPanel presentationPanel = new ScaleToFitJPanel();
GridBagConstraints constraints = new GridBagConstraints();
constraints.weightx = 1;
constraints.weighty = 1;
constraints.fill = GridBagConstraints.BOTH;
panel.add(presentationPanel, constraints);
frame.getContentPane().add(panel);
frame.pack();
Main m = new Main(baseContext, presentationPanel, db, decks, presentations);
Presentation presentation = new Discovery(
baseContext, options.mountPrefix,
options.deckPrefix, options.maxMtScanCount).getPresentation();
logger.info("Using presentation: " + presentation);
m.joinPresentation(presentation, options.joinTimeoutSeconds, options.slideRowFormat);
}
}
public Main(VContext context, ImageViewer viewer, Database db, Table decks,
Table presentations) throws VException {
this.context = context;
this.db = db;
this.presentations = presentations;
this.decks = decks;
this.viewer = viewer;
}
public void joinPresentation(final Presentation presentation,
int joinTimeoutSeconds,
String slideRowFormat) throws VException {
Syncgroup syncgroup = db.getSyncgroup(presentation.getSyncgroupName());
syncgroup.join(context.withTimeout(Duration.standardSeconds(joinTimeoutSeconds)),
new SyncgroupMemberInfo((byte) 1, false));
for (String member : syncgroup.getMembers(context).keySet()) {
logger.info("Member: " + member);
}
for (KeyValue keyValue : presentations.scan(context, RowRange.prefix(""))) {
System.out.println("Presentation: " + keyValue);
}
BatchDatabase batch = db.beginBatch(context, null);
ResumeMarker marker = batch.getResumeMarker(context);
String rowKey = Joiner.on("/").join(presentation.getDeckId(), presentation
.getPresentationId(), "CurrentSlide");
logger.info("going to watch row key " + rowKey);
VIterable<WatchChange> changes = db.watch(context, presentations.name(), rowKey, marker);
for (WatchChange change : changes) {
logger.info("Change detected in " + change.getRowName());
logger.info("Type: " + change.getChangeType());
try {
VCurrentSlide currentSlide = (VCurrentSlide) VomUtil.decode(change.getVomValue(),
VCurrentSlide.class);
logger.info("Current slide: " + currentSlide);
// Read the corresponding slide.
String row = String.format(slideRowFormat, presentation.getDeckId(),
currentSlide.getNum());
VSlide slide = (VSlide) decks.getRow(row).get(context, VSlide.class);
final BufferedImage image = ImageIO.read(
new ByteArrayInputStream(slide.getThumbnail()));
viewer.setImage(image);
} catch (IOException | VException e) {
logger.log(Level.WARNING, "exception encountered while handling change event", e);
}
}
if (changes.error() != null) {
logger.log(Level.WARNING, "Premature end of slide changes: " + changes.error());
}
}
private static void enableOSXFullscreen(Window window) {
Preconditions.checkNotNull(window);
try {
// This class may not be present on the system (e.g. if we're not on MacOSX),
// use reflection so that we can make this an optional dependency.
Class util = Class.forName("com.apple.eawt.FullScreenUtilities");
Class params[] = new Class[]{Window.class, Boolean.TYPE};
@SuppressWarnings({"unchecked", "rawtypes"})
Method method = util.getMethod("setWindowCanFullScreen", params);
method.invoke(util, window, true);
} catch (ClassNotFoundException e) {
// Probably not on Mac OS X
} catch (Exception e) {
logger.log(Level.WARNING, "Couldn't enable fullscreen on Mac OS X", e);
}
}
public static class Options {
@Parameter(names = {"-s", "--storageRootDir"},
description = "the root directory to use for local storage")
private String storageRootDir = Joiner.on(File.separator).join(
System.getProperty("java.io.tmpdir"), "syncslidepresenter-storage");
@Parameter(names = {"-d", "--deckPrefix"},
description = "mounttable prefix for live presentations.")
private String deckPrefix = "happyDeck";
@Parameter(names = {"-m", "--mountPrefix"}, description = "the base path in the namespace"
+ " where the syncbase service will be mounted")
private String mountPrefix = "/192.168.86.254:8101";
@Parameter(names = {"--joinTimeout"},
description = "the number of seconds to wait to join the presentation")
private int joinTimeoutSeconds = 10;
@Parameter(names = {"--maxMtScanCount"},
description = "max number of times to scan MT looking for presentations.")
private int maxMtScanCount = 10;
@Parameter(names = {"-f", "--slideRowFormat"},
description = "a pattern specifying where slide rows are found")
private String slideRowFormat = "%s/slides/%04d";
@Parameter(names = {"-h", "--help"}, description = "display this help message", help = true)
private boolean help = false;
}
private static class ScaleToFitJPanel extends JPanel implements ImageViewer {
private Image image;
public ScaleToFitJPanel() {
super();
setPreferredSize(new Dimension(250, 250));
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
repaint();
}
});
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (image != null) {
int width;
int height;
double containerRatio = 1.0d * getWidth() / getHeight();
double imageRatio = 1.0d * image.getWidth(null) / image.getHeight(null);
if (containerRatio < imageRatio) {
width = getWidth();
height = (int) (getWidth() / imageRatio);
} else {
width = (int) (getHeight() * imageRatio);
height = getHeight();
}
// Center the image in the container.
int x = (int) (((double) getWidth() / 2) - ((double) width / 2));
int y = (int) (((double) getHeight() / 2) - ((double) height / 2));
g.drawImage(image, x, y, width, height, this);
}
}
@Override
public void setImage(Image image) {
this.image = image;
setPreferredSize(new Dimension(image.getWidth(null), image.getHeight(null)));
repaint();
}
}
private interface ImageViewer {
void setImage(Image image);
}
private static class Discovery {
public static final Duration MT_TIMEOUT =
Duration.standardSeconds(10);
private final VContext context;
private final String mtName;
private final String deckPrefix;
private final int maxMtScanCount;
public Discovery(VContext context, String mtName, String deckPrefix, int maxMtScanCount) {
this.context = context;
this.mtName = mtName;
this.deckPrefix = deckPrefix;
this.maxMtScanCount = maxMtScanCount;
}
private Set<String> scan(String pattern) throws VException {
logger.info("Scanning MT " + mtName + " with pattern \"" + pattern + "\"");
Namespace ns = V.getNamespace(context);
ns.setRoots(ImmutableList.of(mtName));
VContext ctx = context.withTimeout(MT_TIMEOUT);
Set<String> result = new HashSet<>();
for (int i = 0; i < maxMtScanCount; i++) {
if (i > 0) {
// Wait a little before trying again.
Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
}
for (GlobReply reply : V.getNamespace(ctx).glob(ctx, pattern)) {
if (reply instanceof GlobReply.Entry) {
MountEntry entry = ((GlobReply.Entry) reply).getElem();
result.add(entry.getName());
for (MountedServer server : entry.getServers()) {
logger.info(" endPoint: \"" + server.getServer() + "\"");
}
}
}
if (!result.isEmpty()) {
return result;
}
}
throw new IllegalStateException(
"Unable to find service matching " + pattern +
" after " + maxMtScanCount + " attempts.");
}
private Presentation findPreso(String serviceName) throws VException {
V.getNamespace(context).flushCacheEntry(context, serviceName);
ParticipantClient client =
ParticipantClientFactory.getParticipantClient(serviceName);
return client.get(context.withTimeout(
Duration.standardSeconds(5)));
}
public Presentation getPresentation() throws VException {
Set<String> services = scan(deckPrefix + "/*");
// Just grab the first one.
return findPreso(services.iterator().next());
}
}
}