io.v.syncslidepresenter.Main.java Source code

Java tutorial

Introduction

Here is the source code for io.v.syncslidepresenter.Main.java

Source

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