Java tutorial
// 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()); } } }