swingx-master/swingx-core/src/main/java/org/jdesktop/swingx/hyperlink/HyperlinkAction.java
/*
 * $Id$
 *
 * Copyright 2009 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
package org.jdesktop.swingx.hyperlink;

import java.awt.Desktop;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Desktop.Action;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.net.URI;
import java.util.logging.Logger;

/**
 * A implementation wrapping <code>Desktop</code> actions BROWSE and MAIL, that is
 * URI-related. 
 * 
 * @author Jeanette Winzenburg
 */
public class HyperlinkAction extends AbstractHyperlinkAction<URI> {
    @SuppressWarnings("unused")
    private static final Logger LOG = Logger.getLogger(HyperlinkAction.class
            .getName());
    
    private Action desktopAction;
    private URIVisitor visitor;
    
    /**
     * Factory method to create and return a HyperlinkAction for the given uri. Tries
     * to guess the appropriate type from the uri. If uri is not null and has a
     * scheme of mailto, create one of type Mail. In all other cases, creates one
     * for BROWSE.
     * 
     * @param uri to uri to create a HyperlinkAction for, maybe null.
     * @return a HyperlinkAction for the given URI.
     * @throws HeadlessException if {@link
     * GraphicsEnvironment#isHeadless()} returns {@code true}
     * @throws UnsupportedOperationException if the current platform doesn't support
     *   Desktop
     */
    public static HyperlinkAction createHyperlinkAction(URI uri) {
        Action type = isMailURI(uri) ? Action.MAIL : Action.BROWSE;
        return createHyperlinkAction(uri, type);
    }
    
    /**
     * Creates and returns a HyperlinkAction with the given target and action type.
     * @param uri the target uri, maybe null.
     * @param desktopAction the type of desktop action this class should perform, must be
     *    BROWSE or MAIL
     * @return a HyperlinkAction
     * @throws HeadlessException if {@link
     * GraphicsEnvironment#isHeadless()} returns {@code true}
     * @throws UnsupportedOperationException if the current platform doesn't support
     *   Desktop
     * @throws IllegalArgumentException if unsupported action type 
     */
    public static HyperlinkAction createHyperlinkAction(URI uri, Action type) {
        return new HyperlinkAction(uri, type);
    }

    /**
     * @param uri
     * @return
     */
    private static boolean isMailURI(URI uri) {
        return uri != null && "mailto".equalsIgnoreCase(uri.getScheme());
    }

    /** 
     * Instantiates a HyperlinkAction with action type BROWSE.
     * 
     * @throws HeadlessException if {@link
     * GraphicsEnvironment#isHeadless()} returns {@code true}
     * @throws UnsupportedOperationException if the current platform doesn't support
     *   Desktop
     * @throws IllegalArgumentException if unsupported action type 
     */
    public HyperlinkAction() {
        this(Action.BROWSE);
    }
    
    /**
     * Instantiates a HyperlinkAction with the given action type.
     * 
     * @param desktopAction the type of desktop action this class should perform, must be
     *    BROWSE or MAIL
     * @throws HeadlessException if {@link
     * GraphicsEnvironment#isHeadless()} returns {@code true}
     * @throws UnsupportedOperationException if the current platform doesn't support
     *   Desktop
     * @throws IllegalArgumentException if unsupported action type 
     */ 
    public HyperlinkAction(Action desktopAction) {
        this(null, desktopAction);
    }

    /**
     * 
     * @param uri the target uri, maybe null.
     * @param desktopAction the type of desktop action this class should perform, must be
     *    BROWSE or MAIL
     * @throws HeadlessException if {@link
     * GraphicsEnvironment#isHeadless()} returns {@code true}
     * @throws UnsupportedOperationException if the current platform doesn't support
     *   Desktop
     * @throws IllegalArgumentException if unsupported action type 
     */
    public HyperlinkAction(URI uri, Action desktopAction) {
        super();
        if (!Desktop.isDesktopSupported()) {
            throw new UnsupportedOperationException("Desktop API is not " +
                                                    "supported on the current platform");
        }
        if (desktopAction != Desktop.Action.BROWSE && desktopAction != Desktop.Action.MAIL) {
           throw new IllegalArgumentException("Illegal action type: " + desktopAction + 
                   ". Must be BROWSE or MAIL");
        }
        this.desktopAction = desktopAction;
        getURIVisitor();
        setTarget(uri);
    }
    
    /**
     * {@inheritDoc} <p>
     * 
     * Implemented to perform the appropriate Desktop action if supported on the current
     * target. Sets the visited property to true if the desktop action doesn't throw
     * an exception or to false if it did. 
     * 
     * Does nothing if the action isn't supported.  
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        if (!getURIVisitor().isEnabled(getTarget())) return;
        try {
            getURIVisitor().visit(getTarget());
            setVisited(true);
        } catch (IOException e1) {
            setVisited(false);
            LOG.fine("cant visit Desktop " + e);
        }
    }
    
    /**
     * @return
     */
    public Action getDesktopAction() {
        return desktopAction;
    }
    
    
    @Override
    protected void installTarget() {
        // doohh ... this is called from super's constructor before we are 
        // fully initialized
        if (visitor == null) return;
        super.installTarget();
        updateEnabled();
    }
    
    /**
     * 
     */
    private void updateEnabled() {
        setEnabled(getURIVisitor().isEnabled(getTarget()));
    }

    /**
     * @return
     */
    private URIVisitor getURIVisitor() {
        if (visitor == null) {
            visitor = createURIVisitor();
        }
        return visitor;
    }

    /**
     * @return
     */
    private URIVisitor createURIVisitor() {
        return getDesktopAction() == Action.BROWSE ?
                new BrowseVisitor() : new MailVisitor();
    }

    /**
     * Thin wrapper around Desktop functionality to allow uniform handling of
     * different actions in HyperlinkAction.
     * 
     */
    private abstract class URIVisitor {
        protected boolean desktopSupported = Desktop.isDesktopSupported();
        
        /**
         * Returns a boolean indicating whether the action is supported on the
         * given URI. This implementation returns true if both the Desktop is 
         * generally supported and <code>isActionSupported()</code>.
         * 
         * PENDING JW: hmm ... which class exactly has to check for valid combination
         * of Action and URI?
         * 
         * @param uri
         * @return
         * 
         * @see #isActionSupported()
         */
        public boolean isEnabled(URI uri) {
            return desktopSupported && isActionSupported();
        }
        
        /**
         * Visits the given URI via Desktop functionality. Must not be called if not
         * enabled.
         * 
         * @param uri the URI to visit
         * @throws IOException if the Desktop method throws IOException.
         * 
         */
        public abstract void visit(URI uri) throws IOException;
        
        /**
         * Returns a boolean indicating if the action is supported by the current 
         * Desktop.
         * 
         * @return true if the Action is supported by the current desktop, false
         * otherwise.
         */
        protected abstract boolean isActionSupported();
    }
    
    private class BrowseVisitor extends URIVisitor {

        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to message the browse method of Desktop.
         */
        @Override
        public void visit(URI uri) throws IOException {
            Desktop.getDesktop().browse(uri);
        }

        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to query the Desktop for support of BROWSE action.
         */
        @Override
        protected boolean isActionSupported() {
            return Desktop.getDesktop().isSupported(Desktop.Action.BROWSE);
        }

        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to guard against null URI in addition to super.
         */
        @Override
        public boolean isEnabled(URI uri) {
            return uri != null && super.isEnabled(uri);
        }
        
        
    }
    
    private class MailVisitor extends URIVisitor {

        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to message the mail function of Desktop.
         */
        @Override
        public void visit(URI uri) throws IOException {
            if (uri == null) {
                Desktop.getDesktop().mail();
            } else {
                Desktop.getDesktop().mail(uri);
            }
        }
        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to query the Desktop for support of MAIL action.
         */
        @Override
        protected boolean isActionSupported() {
            return Desktop.getDesktop().isSupported(Desktop.Action.MAIL);
        }
        
    }
}
