filter_list

More radio channels

Saturday 22.10.2016

I've been extending the radio application that I've made earlier with a few more channels. These channels are found at ShoutCast. This service contains well over 75 thousands of internet broadcasted radio channels, all in various broadcast quality.

I also added functionality for adjusting playback volume and pausing the radio stream.

Parrots in space

Sunday 16.10.2016

Started on a visualization of ParrotPlay in space. This visualization tries to imitate the 5th season Doctor Who introduction, where credits are revealed inside of a tunnel.

Parrot themed radio application

Thursday 13.10.2016

Started working on a Parrot themed radio application. With this application it is easy to start playback of norwegian broadcast channels. All audio is transferred from the services own web servers.

As the FM radio signal in our region will be shut down on the 8th of february, we are planning to build a radio app that streams data from norwegian broadcasts through the internet, for those that still has not migrated to DAB. This application served as a prototype for how this could be done.

Start radio

Start work at ParrotPlay

Monday 19.09.2016

Today I started in a new job at ParrotPlay. Here I will be working on their streaming platform. Using this application, users will be able gather all theirs streaming services into one application. By doing this, they will be able to search for and play back content independent on where the movie itself are stored.

OpenSAML and Feide integration

Thursday 26.06.2014

Recently I worked on implementing an integration towards Feide, a Norwegian Single Sign-On login service. This service uses SAML 2.0 for communication between a Service Provider (SP) and an Identity Provider (IdP). There exists a library tailored for Feide, but this project has not been updated since 2011. I've used this library for an earlier Feide integration, but I wanted a more stable solution. The most well-known library out there for communication with a SAML server are currently OpenSAML.

Although OpenSAML comes highly recommended, there are close to no documentation on its usage. Their Wiki has no full example on its usage, and all you have are some tidbits here and there. Thankfully I found a guy that wrote a lot of blog posts on the topic, and I managed to scrape together some working code from these posts and various other sources online.

This is the main login class I ended up with:


import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.security.InvalidParameterException;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.opensaml.Configuration;
import org.opensaml.DefaultBootstrap;
import org.opensaml.common.SAMLObject;
import org.opensaml.common.binding.BasicSAMLMessageContext;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.binding.encoding.HTTPRedirectDeflateEncoder;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.Attribute;
import org.opensaml.saml2.core.AttributeStatement;
import org.opensaml.saml2.core.AuthnContextClassRef;
import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration;
import org.opensaml.saml2.core.AuthnRequest;
import org.opensaml.saml2.core.Issuer;
import org.opensaml.saml2.core.NameIDPolicy;
import org.opensaml.saml2.core.RequestedAuthnContext;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.core.Statement;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml2.metadata.SingleSignOnService;
import org.opensaml.saml2.metadata.provider.MetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider;
import org.opensaml.security.MetadataCredentialResolver;
import org.opensaml.security.MetadataCredentialResolverFactory;
import org.opensaml.security.MetadataCriteria;
import org.opensaml.util.resource.ClasspathResource;
import org.opensaml.util.resource.ResourceException;
import org.opensaml.ws.message.encoder.MessageEncodingException;
import org.opensaml.ws.transport.http.HttpServletResponseAdapter;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.XMLObjectBuilder;
import org.opensaml.xml.io.Unmarshaller;
import org.opensaml.xml.io.UnmarshallingException;
import org.opensaml.xml.parse.BasicParserPool;
import org.opensaml.xml.schema.XSString;
import org.opensaml.xml.security.CriteriaSet;
import org.opensaml.xml.security.credential.Credential;
import org.opensaml.xml.security.criteria.EntityIDCriteria;
import org.opensaml.xml.util.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

public class FeideLogin implements ExternalLogin {

    private static final Log LOGGER = LogFactory.getLog(FeideLogin.class);
    private static final Map<Class<?>, QName> ELEMENT_CACHE = new ConcurrentHashMap<Class<?>, QName>();
    private static final String CALLBACK = "https://***?provider=feide";
    private static final String SAML2_ISSUER = "***";

    public FeideLogin() {
        try {
            DefaultBootstrap.bootstrap();
        } catch (final ConfigurationException e) {
            throw new RuntimeException("Bootstrapping failed");
        }
    }

    @Override
    public final void doLogin(final HttpServletResponse response) {
        final MetadataProvider metadataProvider = this.getMetadataProvider();
        final EntityDescriptor entityDescriptor = this.getEntityDescriptor(metadataProvider);
        if (entityDescriptor == null) {
            try {
                response.sendRedirect("/login");
            } catch (final IOException ex) {
                LOGGER.warn("Failed to send user back to login page", ex);
            }
            return;
        }

        final AuthnRequest authnRequest = this.generateAuthnRequest(entityDescriptor);

        final HttpServletResponseAdapter responseAdapter = new HttpServletResponseAdapter(response, true);
        final BasicSAMLMessageContext<SAMLObject, AuthnRequest, SAMLObject> context = new BasicSAMLMessageContext<>();
        context.setPeerEntityEndpoint(this.getSingleSignOnService(entityDescriptor));
        context.setOutboundSAMLMessage(authnRequest);
        context.setOutboundSAMLMessageSigningCredential(this.generateCredential(metadataProvider));
        context.setOutboundMessageTransport(responseAdapter);
        context.setRelayState(CALLBACK);

        final HTTPRedirectDeflateEncoder encoder = new HTTPRedirectDeflateEncoder();

        try {
            encoder.encode(context);
        } catch (final MessageEncodingException ex) {
            LOGGER.warn("Error while performing Feide login", ex);
        }
    }

    @Override
    public final ExternalUserMetadata getUserMetadata(final HttpServletRequest request) {
        final String samlResponse = request.getParameter("SAMLResponse");
        if (samlResponse == null) {
            // throw new IllegalStateException("SAMLResponse parameter cannot be null");
            return null;
        }

        try {
            final String xml = new String(Base64.decode(samlResponse), "UTF-8");
            final XMLObject object = this.unmarshallElementFromString(xml);
            if (!(object instanceof Response)) {
                throw new IllegalArgumentException("SAMLResponse must be of type Response. Was " + object);
            }

            final ExternalUserMetadata metadata = new ExternalUserMetadata();
            metadata.setProvider(LoginProvider.FEIDE);
            final Response response = (Response) object;

            for (final Assertion assertion : response.getAssertions()) {
                for (final Statement statement : assertion.getStatements()) {
                    if (statement instanceof AttributeStatement) {
                        final AttributeStatement attributeStatement = (AttributeStatement) statement;
                        for (final Attribute attribute : attributeStatement.getAttributes()) {
                            if ("displayName".equals(attribute.getName())) {
                                metadata.setName(this.getValue(attribute));
                            }
                            if ("eduPersonTargetedID".equals(attribute.getName())) {
                                metadata.setId(this.getValue(attribute));
                            }
                        }
                    }
                }
            }

            return metadata;
        } catch (final UnsupportedEncodingException ex) {
            throw new RuntimeException(ex);
        }
    }

    private String getValue(final Attribute attribute) {
        for (final XMLObject object : attribute.getAttributeValues()) {
            if (object instanceof XSString) {
                return ((XSString) object).getValue();
            }
        }
        return null;
    }

    private MetadataProvider getMetadataProvider() {
        final String basePath = FeideLogin.class.getPackage().getName().replaceAll("\\.", "/");
        final String path = "/" + basePath + "/feide-idp-metadata.xml";

        try {
            final ClasspathResource resource = new ClasspathResource(path);
            final ResourceBackedMetadataProvider metadataProvider =
                    new ResourceBackedMetadataProvider(new Timer(true), resource);
            metadataProvider.setRequireValidMetadata(true);
            metadataProvider.setParserPool(new BasicParserPool());
            metadataProvider.initialize();
            return metadataProvider;
        } catch (final ResourceException | MetadataProviderException ex) {
            LOGGER.warn("Failed to read service metadata", ex);
        }

        return null;
    }

    private EntityDescriptor getEntityDescriptor(final MetadataProvider metadataProvider) {
        if (metadataProvider != null) {
            try {
                return metadataProvider.getEntityDescriptor("https://idp.feide.no");
            } catch (final MetadataProviderException ex) {
                LOGGER.warn("Failed to read service metadata", ex);
            }
        }

        return null;
    }

    private AuthnRequest generateAuthnRequest(final EntityDescriptor entityDescriptor) {
        final AuthnRequest authnRequest = this.buildXMLObject(AuthnRequest.class);
        authnRequest.setForceAuthn(true);
        authnRequest.setIsPassive(false);
        authnRequest.setIssueInstant(new DateTime(DateTimeZone.UTC));
        authnRequest.setDestination(this.getSingleSignOnLocation(entityDescriptor));
        authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
        authnRequest.setAssertionConsumerServiceURL(CALLBACK);
        authnRequest.setID("_" + UUID.randomUUID().toString());
        // authnRequest.setProviderName("https://idp.feide.no");

        final Issuer issuer = this.buildXMLObject(Issuer.class);
        issuer.setValue(SAML2_ISSUER);
        authnRequest.setIssuer(issuer);

        final NameIDPolicy nameIDPolicy = this.buildXMLObject(NameIDPolicy.class);
        nameIDPolicy.setSPNameQualifier(SAML2_ISSUER);
        nameIDPolicy.setAllowCreate(true);
        nameIDPolicy.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:transient");
        authnRequest.setNameIDPolicy(nameIDPolicy);

        final RequestedAuthnContext requestedAuthnContext = this.buildXMLObject(RequestedAuthnContext.class);
        requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.MINIMUM);
        final AuthnContextClassRef authnContextClassRef = this.buildXMLObject(AuthnContextClassRef.class);
        authnContextClassRef.setAuthnContextClassRef(
                "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");
        requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
        authnRequest.setRequestedAuthnContext(requestedAuthnContext);

        return authnRequest;
    }

    private Credential generateCredential(final MetadataProvider metadataProvider) {
        final MetadataCredentialResolver resolver = MetadataCredentialResolverFactory.getFactory()
                .getInstance(metadataProvider);

        final CriteriaSet criteriaSet = new CriteriaSet();
        criteriaSet.add(new MetadataCriteria(IDPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS));
        criteriaSet.add(new EntityIDCriteria("IPDEntityId"));

        try {
            return resolver.resolveSingle(criteriaSet);
        } catch (final org.opensaml.xml.security.SecurityException ex) {
            LOGGER.warn("Failed to lookup credentials", ex);
        }

        return null;
    }

    private SingleSignOnService getSingleSignOnService(final EntityDescriptor entityDescriptor) {
        final List<SingleSignOnService> singleSignOnServices =
                entityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS).getSingleSignOnServices();
        for (final SingleSignOnService singleSignOnService : singleSignOnServices) {
            if (singleSignOnService.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
                return singleSignOnService;
            }
        }

        return null;
    }

    private String getSingleSignOnLocation(final EntityDescriptor entityDescriptor) {
        final SingleSignOnService singleSignOnService = this.getSingleSignOnService(entityDescriptor);
        if (singleSignOnService != null) {
            return singleSignOnService.getLocation();
        } else {
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    private <T extends XMLObject> T buildXMLObject(final Class<T> type) {
        try {
            final QName objectQName = this.getElementQName(type);
            final XMLObjectBuilder<T> builder = Configuration.getBuilderFactory().getBuilder(objectQName);
            if (builder == null) {
                throw new InvalidParameterException("No builder exists for object: " + objectQName.getLocalPart());
            }
            return builder.buildObject(objectQName.getNamespaceURI(), objectQName.getLocalPart(),
                    objectQName.getPrefix());
        } catch (final SecurityException e) {
            throw new RuntimeException(e);
        }
    }

    private <T> QName getElementQName(final Class<T> type) {
        if (ELEMENT_CACHE.containsKey(type)) {
            return ELEMENT_CACHE.get(type);
        }

        try {
            Field typeField;
            try {
                typeField = type.getDeclaredField("DEFAULT_ELEMENT_NAME");
            } catch (final NoSuchFieldException ex) {
                typeField = type.getDeclaredField("ELEMENT_NAME");
            }

            final QName objectQName = (QName) typeField.get(null);
            ELEMENT_CACHE.put(type, objectQName);
            return objectQName;
        } catch (final NoSuchFieldException e) {
            throw new RuntimeException(e);
        } catch (final IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private XMLObject unmarshallElementFromString(final String elementString) {
        try {
            final Element samlElement = this.loadElementFromString(elementString);

            final Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(samlElement);
            if (unmarshaller == null) {
                LOGGER.error("Unable to retrieve unmarshaller by DOM Element");
                throw new IllegalArgumentException("No unmarshaller for " + elementString);
            }

            return unmarshaller.unmarshall(samlElement);
        } catch (final UnmarshallingException ex) {
            LOGGER.error("Unmarshalling failed when parsing element string " + elementString, ex);
            throw new RuntimeException(ex);
        }
    }

    private Element loadElementFromString(final String elementString) {
        try {
            final DocumentBuilderFactory newFactory = this.getDocumentBuilderFactory();
            newFactory.setNamespaceAware(true);

            final DocumentBuilder builder = newFactory.newDocumentBuilder();

            final Document doc = builder.parse(new ByteArrayInputStream(elementString.getBytes("UTF-8")));
            final Element samlElement = doc.getDocumentElement();

            return samlElement;
        } catch (final ParserConfigurationException ex) {
            LOGGER.error("Unable to parse element string " + elementString, ex);
            throw new RuntimeException(ex);
        } catch (final SAXException ex) {
            LOGGER.error("Unable to parse element string " + elementString, ex);
            throw new RuntimeException(ex);
        } catch (final IOException ex) {
            LOGGER.error("Unable to parse element string " + elementString, ex);
            throw new RuntimeException(ex);
        }
    }

    private DocumentBuilderFactory getDocumentBuilderFactory() throws ParserConfigurationException {
        final DocumentBuilderFactory newFactory = DocumentBuilderFactory.newInstance();
        newFactory.setNamespaceAware(true);

        // External entities has been disabled in order to prevent XML External Entity (XXE) attacks.
        newFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        return newFactory;
    }
}

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public interface ExternalLogin {

    void doLogin(HttpServletRequest request, HttpServletResponse response);

    ExternalUserMetadata getUserMetadata(HttpServletRequest request);
}

import java.io.Serializable;

import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;

@SuppressWarnings("serial")
@JsonIgnoreProperties(ignoreUnknown = true)
public class ExternalUserMetadata implements Serializable {

    private LoginProvider provider;
    private String id;
    private String name;
    private String firstName;
    private String lastName;
    private String link;
    private String username;
    private String gender;
    private String locale;
    private String hostedDomain;

    public final LoginProvider getProvider() {
        return this.provider;
    }

    public final void setProvider(final LoginProvider provider) {
        this.provider = provider;
    }

    public final String getId() {
        return this.id;
    }

    public final void setId(final String id) {
        this.id = id;
    }

    @JsonProperty("sub")
    public final void setSub(final String sub) {
        this.id = sub;
    }

    public final String getName() {
        return this.name;
    }

    public final void setName(final String name) {
        this.name = name;
    }

    public final String getFirstName() {
        return this.firstName;
    }

    @JsonProperty("first_name")
    public final void setFirstName(final String firstName) {
        this.firstName = firstName;
    }

    @JsonProperty("given_name")
    public final void setGivenName(final String givenName) {
        this.firstName = givenName;
    }

    public final String getLastName() {
        return this.lastName;
    }

    @JsonProperty("last_name")
    public final void setLastName(final String lastName) {
        this.lastName = lastName;
    }

    @JsonProperty("family_name")
    public final void setFamilyName(final String familyName) {
        this.lastName = familyName;
    }

    public final String getLink() {
        return this.link;
    }

    public final void setLink(final String link) {
        this.link = link;
    }

    public final String getUsername() {
        return this.username;
    }

    public final void setUsername(final String username) {
        this.username = username;
    }

    @JsonProperty("screen_name")
    public final void setScreenName(final String screenName) {
        this.username = screenName;
    }

    public final String getGender() {
        return this.gender;
    }

    public final void setGender(final String gender) {
        this.gender = gender;
    }

    public final String getLocale() {
        return this.locale;
    }

    public final void setLocale(final String locale) {
        this.locale = locale;
    }

    public final String getHostedDomain() {
        return this.hostedDomain;
    }

    public final void setHostedDomain(final String hostedDomain) {
        this.hostedDomain = hostedDomain;
    }

    @JsonProperty("hd")
    public final void setHD(final String hd) {
        this.hostedDomain = hd;
    }
}

public enum LoginProvider {

    FACEBOOK, FEIDE, GOOGLE, MICROSOFT, TWITTER
}

As one might see from this code, a lot of manual work goes into the SAML communication. Hopefully this example will help someone else in the same situation.

First published at:
https://infposs.blogspot.no/2014/06/opensaml-and-feide-integration.html

Tunnel render method

Wednesday 01.01.2014

I've updated the Tunnel Effect to allow selection of render method. Previously the render method was selected automatically based on if the browser supports typed arrays / buffers. By adding this feature, one might compare the performance difference between these methods of rendering within the browser.

What I noticed from doing so, was that since I originally made this demo, back in February 2013, both Chrome and Firefox has improved their implementations for arrays. As of Chrome 31 and Firefox 26, their array implementation are almost identical in speed of the buffer implementation. IE 11 does not support buffers yet, and the demo gives an error message if you try to activate buffers there.

JavaScript Asteroids Emulator

Sunday 14.07.2013

Lately I've been working on a JavaScript-based Asteroids emulator. I've always wondered how a CPU works. Through this project I've gained a lot of insight into how the black box of a CPU really works. The end result of this project is this emulator, and it is now finally ready for the general public.

The CPUs of the past is a lot easier to look into than the current generation, mostly because of increased complexity. I therefore decided to look into the MOS 6502 CPU that was popular in the 70's and 80's. This was the CPU family that powered classics from the arcade as well as home computers like Atari 2600, Commodore 64 and Apple 2.

This emulator works best with Chrome. You may also use FireFox, but you will not get audio, as it does not support Web Audio API yet. This is expected to be available in a later version of the browser. Internet Explorer does not yet support typed arrays, but this is expected to be added alongside WebGL in version 11.

Because this game is still the property of Atari Inc., I cannot legally provide the ROMs required to run this game from this site. If you are in the possession of said ROMs, you may upload these files to your local browser for use with this emulator. These files will remain on your computer only, and will be read from your browser should you choose to visit this page again. This emulator is compatible with the same game ROMs as the latest version of MAME, as of writing version 0.149.

Full source is available for this emulator if you try to view its sources. I've tried to make the source as readable as possible, and hope that you'll learn something from reading these.

Go to Asteroids emulator

InstallCert and Java 7

Thursday 20.06.2013

When communicating with a server using a self-signed SSL using a Java based client, this custom certificate must be known by the callee side of the communication. This can be done manually by adding the certificate to the client JVM.

To simplify this process, one can use a simple tool called InstallCert as described here and here. This application was originally written for Java 5 / Java 6. While this solution still works, when you try to run this application through Java 7, you will get an additional error message like this:

javax.net.ssl.SSLException: java.lang.UnsupportedOperationException
 at sun.security.ssl.Alerts.getSSLException(Alerts.java:208)
 at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1886)
 at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1844)
 at sun.security.ssl.SSLSocketImpl.handleException(SSLSocketImpl.java:1827)
 at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1346)
 at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323)
 at org.vangen.auth.InstallCert.main(InstallCert.java:81)
Caused by: java.lang.UnsupportedOperationException
 at org.vangen.auth.InstallCert$SavingTrustManager.getAcceptedIssuers(InstallCert.java:168)
 at sun.security.ssl.AbstractTrustManagerWrapper.checkAlgorithmConstraints(SSLContextImpl.java:926)
 at sun.security.ssl.AbstractTrustManagerWrapper.checkAdditionalTrust(SSLContextImpl.java:872)
 at sun.security.ssl.AbstractTrustManagerWrapper.checkServerTrusted(SSLContextImpl.java:814)
 at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1323)
 at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:153)
 at sun.security.ssl.Handshaker.processLoop(Handshaker.java:868)
 at sun.security.ssl.Handshaker.process_record(Handshaker.java:804)
 at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1016)
 at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312)
 at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339)
 ... 2 more
 

To avoid this error, you will need to change how accepted issuers are returned. The original code threw an exception for this method. The Java 7 implementation calls this method, which was not the case with earlier versions of Java. The solution to this problem is to simply return an empty array like this:


    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
        // throw new UnsupportedOperationException();
    }

By doing this small change, the application will no longer return an error upon execution. The full application code will then be:


package org.vangen.auth;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

public class InstallCert {

    public static void main(final String[] args) throws Exception {
        String host;
        int port;
        char[] passphrase;
        if ((args.length == 1) || (args.length == 2)) {
            final String[] c = args[0].split(":");
            host = c[0];
            port = (c.length == 1) ? 443 : Integer.parseInt(c[1]);
            final String p = (args.length == 1) ? "changeit" : args[1];
            passphrase = p.toCharArray();
        } else {
            System.out.println(
                    "Usage: java InstallCert <host>[:port] [passphrase]");
            return;
        }

        File file = new File("jssecacerts");
        if (file.isFile() == false) {
            final char SEP = File.separatorChar;
            final File dir = new File(System.getProperty("java.home")
                    + SEP + "lib" + SEP + "security");
            file = new File(dir, "jssecacerts");
            if (file.isFile() == false) {
                file = new File(dir, "cacerts");
            }
        }

        System.out.println("Loading KeyStore " + file + "...");
        final InputStream in = new FileInputStream(file);
        final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        ks.load(in, passphrase);
        in.close();

        final SSLContext context = SSLContext.getInstance("TLS");
        final TrustManagerFactory tmf =
                TrustManagerFactory.getInstance(TrustManagerFactory
                        .getDefaultAlgorithm());
        tmf.init(ks);
        final X509TrustManager defaultTrustManager =
                (X509TrustManager) tmf.getTrustManagers()[0];
        final SavingTrustManager tm = new SavingTrustManager(
                defaultTrustManager);
        context.init(null, new TrustManager[] { tm }, null);
        final SSLSocketFactory factory = context.getSocketFactory();

        System.out.println("Opening connection to "
                + host + ":" + port + "...");
        final SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
        socket.setSoTimeout(10000);
        try {
            System.out.println("Starting SSL handshake...");
            socket.startHandshake();
            socket.close();
            System.out.println();
            System.out.println("No errors, certificate is already trusted");
        } catch (final SSLException e) {
            System.out.println();
            e.printStackTrace(System.out);
        }

        final X509Certificate[] chain = tm.chain;
        if (chain == null) {
            System.out.println("Could not obtain server certificate chain");
            return;
        }

        final BufferedReader reader =
                new BufferedReader(new InputStreamReader(System.in));

        System.out.println();
        System.out.println("Server sent " + chain.length + " certificate(s):");
        System.out.println();
        final MessageDigest sha1 = MessageDigest.getInstance("SHA1");
        final MessageDigest md5 = MessageDigest.getInstance("MD5");
        for (int i = 0; i < chain.length; i++) {
            final X509Certificate cert = chain[i];
            System.out.println(" " + (i + 1) + " Subject "
                    + cert.getSubjectDN());
            System.out.println("   Issuer  " + cert.getIssuerDN());
            sha1.update(cert.getEncoded());
            System.out.println("   sha1    " + toHexString(sha1.digest()));
            md5.update(cert.getEncoded());
            System.out.println("   md5     " + toHexString(md5.digest()));
            System.out.println();
        }

        System.out.println("Enter certificate to add to trusted keystore"
                + " or 'q' to quit: [1]");
        final String line = reader.readLine().trim();
        int k;
        try {
            k = (line.length() == 0) ? 0 : Integer.parseInt(line) - 1;
        } catch (final NumberFormatException e) {
            System.out.println("KeyStore not changed");
            return;
        }

        final X509Certificate cert = chain[k];
        final String alias = host + "-" + (k + 1);
        ks.setCertificateEntry(alias, cert);

        final OutputStream out = new FileOutputStream(file);
        ks.store(out, passphrase);
        out.close();

        System.out.println();
        System.out.println(cert);
        System.out.println();
        System.out.println(
                "Added certificate to keystore 'cacerts' using alias '"
                        + alias + "'");
    }

    private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();

    private static String toHexString(final byte[] bytes) {
        final StringBuilder sb = new StringBuilder(bytes.length * 3);
        for (int b : bytes) {
            b &= 0xff;
            sb.append(HEXDIGITS[b >> 4]);
            sb.append(HEXDIGITS[b & 15]);
            sb.append(' ');
        }
        return sb.toString();
    }

    private static class SavingTrustManager implements X509TrustManager {

        private final X509TrustManager tm;
        private X509Certificate[] chain;

        SavingTrustManager(final X509TrustManager tm) {
            this.tm = tm;
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
            // throw new UnsupportedOperationException();
        }

        @Override
        public void checkClientTrusted(final X509Certificate[] chain,
                final String authType)
                throws CertificateException {
            throw new UnsupportedOperationException();
        }

        @Override
        public void checkServerTrusted(final X509Certificate[] chain,
                final String authType)
                throws CertificateException {
            this.chain = chain;
            this.tm.checkServerTrusted(chain, authType);
        }
    }
}

First published at:
http://infposs.blogspot.no/2013/06/installcert-and-java-7.html

Restlet and Spring integration

Friday 14.06.2013
Restlet supports publication of their services either through a servlet or an indepdendent server out of the box. To better integrate the REST service into a Spring MVC framework, I created some glue to better bind them together. While Restlet supports Spring through their own Spring integration, I wanted to avoid the use of a an another servlet for the Restlet integration. Instead I wanted to use an automatically configured Spring MVC controller. In addition I wanted to use the new Spring JavaConfig for configuring the Spring Beans.

The first step was to create a Spring MVC controller, wrapping REST requests:


@Controller
@RequestMapping("/rest")
public class RestletController {

    @Autowired
    private Restlet root;

    @Autowired
    private Context context;

    private ServletAdapter adapter;

    public RestletController() {
    }

    @PostConstruct
    public final void postConstruct() {
        final Application application = new Application(this.context);
        application.setInboundRoot(this.root);

        this.adapter = new ServletAdapter(new DummyServletContext());
        this.adapter.setNext(application);
    }

    @RequestMapping("/**")
    public final void request(final HttpServletRequest request,
            final HttpServletResponse response)
            throws ServletException, IOException {
        this.adapter.service(request, response);
    }
}

This controller sets up the Restlet application when all dependencies has been injected. This happens in the method annotated with a @PostContruct annotation. The ServletAdapter class is normally used in the Restlet servlet, but are here used as a target for forwarding the request and response objects. This will result in requests being handles as if the servlet was configured for the application container.

This application uses Spring JavaConfig for configuring the application, and this is the configuration class for setting up the Restlet environment:


@Configuration
public class RestletConfig {

    @Bean
    public Restlet root() {
        final SpringRouter router = new SpringRouter(this.restletContext());
        router.attach("/users", new SpringFinder() {
            @Override
            public ServerResource create() {
                // lookup-method
                return RestletConfig.this.usersResource();
            }
        });
        router.attach("/users/{domain}/{username}", new SpringFinder() {
            @Override
            public ServerResource create() {
                // lookup-method
                return RestletConfig.this.userResource();
            }
        });

        return router;
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    public Context restletContext() {
        return new Context();
    }

    @Bean
    public ServiceSpringSecurityVerifier verifier() {
        return new ServiceSpringSecurityVerifier();
    }

    @Bean
    public UsersResource usersResource() {
        return new UsersResource();
    }

    @Bean
    public UserResource userResource() {
        return new UserResource();
    }
}

This class configures routes for the REST service, and utilizes the Restlet Spring integration to map server resources to Spring beans. For each mapping we can see that the create method looks up beans from the Spring container. By doing this, we are able to utilize injection of dependencies for our server resources.

This is the server resource for list of users:


public class UsersResource extends ServerResource {

    @Autowired
    private UserDao userDao;

    @Get("json")
    public final Representation getUsers() {
        return this.convertCollection(this.userDao.getAll());
    }

    @Post("json")
    public final void addUser(final User user) {
        this.userDao.persist(user);
    }

    private Representation convertCollection(final Collection<?> collection) {
        final JSONArray result = new JSONArray();
        for (final Object bean : collection) {
            result.put(new JSONObject(bean));
        }
        return new JsonRepresentation(result);
    }
}

This class injects a user DAO service, which in turn fetches users from the database. The first method covers the get-scenario for all users. This request should return a list of all known users. This server resource is also used for adding new users through the post-scenario. Unfortunately Restlet does not handle collections, which means that a method will need to be built to convert a collection into a JSONArray object, which in turn is returned as a list.

This is the server resource for single users:


public class UserResource extends ServerResource {

    @Autowired
    private UserDao userDao;

    @Get("json")
    public final Representation getUser() {
        final String userName = (String)
                this.getRequestAttributes().get("username");
        final User user = this.userDao.get(userName);

        if (user != null) {
            return new JsonRepresentation(user);
        } else {
            this.doError(Status.CLIENT_ERROR_NOT_FOUND);
            return null;
        }
    }

    @Put("json")
    public final void updateUser(final User user) {
        this.userDao.persist(user);
    }

    @Delete
    public final void deleteUser() {
        final String userName = (String)
                this.getRequestAttributes().get("username");
        final User user = this.userDao.get(userName);
        if (user != null) {
            this.userDao.delete(user);
        }
    }
}

In the get-scenario for a single user, you get that user if it can be found. If it cannot be found, a standard error should be given. The put-scenario replaces the user selected. The last method covers the delete-scenario, which simply deletes the selected user from the database.

The more observant reader would probably see that the same method is used for both adding and replacing a user. This is intentional, as we want to make is slightly easier to add and replace users. If a user with a given username is found in either of these scenarios, it will be replaced, Otherwise, the user will be added.

First published at:
http://infposs.blogspot.no/2013/06/restlet-and-spring-integration.html

Hibernate strong versus weak relationship

Thursday 28.03.2013

The problem

When you want to map two entities together using a many-to-many connection, Hibernate needs you to provide a @ManyToMany JPA annotation. When I wanted to connect two entities I initially set up the connection in the following way:


@Table(name = "person")
public class Person extends AbstractDocumentEntity {

    @ManyToMany(mappedBy = "persons")
    private List<PersonGroup> groups;

And the corresponding group entity looks like this:


@Table(name = "person_group")
public class PersonGroup extends AbstractTrackedEntity {

    @ManyToMany
    @JoinTable(name = "person_person_group",
            joinColumns = { @JoinColumn(name = "group_id") },
            inverseJoinColumns = { @JoinColumn(name = "person_id") })
    private List<Person> persons;

This setup works perfectly when you try to read data from the database. Whenever you fetch the base entity, you can also read its connected groups or persons. So far, so good.

The problem lies however in the situation where you want to update the list of connected entities. In its current state, no changes are saved no matter if I add new entities to either of the collections.

Cascading changes

The first change that needed to be done, was to define that changes to the collection should be updated when the main enity gets persisted. One defines this by saying that the collection changes should be cascaded from the main entity. This is defined by adding a cascade property to the connection definition:


    @ManyToMany(mappedBy = "persons", cascade = CascadeType.ALL)
    @ManyToMany(cascade = CascadeType.ALL)

This changed the situation somewhat when I tried to update the collections. When I added entities to the groups collection from the person entity, nothing seemed to happen when I told Hibernate to persist the changes. When I added persons from the group entity, changes were saved. This shows that the cascade property is ignored when you are also using the mappedBy property.

Weak versus strong connection

The problem lies in how Hibernate defines this connection between these two entities. The group entity owns the connection in this case, as it defines what table and fields to use when looking up data. The person entity simply connects to this connection without defining what the connection entails. In this case this means that the connection from the group entity is the strong side, while the connection from the person side is the weak side. What this means in practice, is that no changes will be saved from the weak side, no matter if you define the connection to cascade its changes or not.

The solution to all of this is to define both sides as the strong side, by adding full definition on both ends, and drop the mappedBy property:


@Table(name = "person")
public class Person extends AbstractDocumentEntity {

    @OrderBy("title")
    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "person_person_group",
              joinColumns = { @JoinColumn(name = "person_id") },
              inverseJoinColumns = { @JoinColumn(name = "group_id") })
    private List<PersonGroup> groups;

And the group entity ended up looking like this:


@Table(name = "person_group")
public class PersonGroup extends AbstractTrackedEntity {

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "person_person_group",
               joinColumns = { @JoinColumn(name = "group_id") },
               inverseJoinColumns = { @JoinColumn(name = "person_id") })
    private List<Person> persons;

As a bonus I also added a sort definition from the person entity to make the returned result look better in the application interface.

Final thoughts

The end result is a slightly more convoluted configuration. For me there is no logical reason why the original solution with a cascade property added should not work. I would rather that Hibernate read the cascade property, and from there decide what should be done with the collection, no matter if you use mappedBy or not.

First published at:
http://infposs.blogspot.no/2013/03/hibernate-strong-versus-weak.html

JavaScript water

Monday 18.02.2013

After implementing the tunnel effect I wanted to try an another oldschool demo effect as well. This time I tried to implement a water effect which I found at Water effect.

Go to Water Effect.

Tunnel update

Sunday 17.02.2013

After publishing the tunnel effect, I got some feedback from Jonas Lund on Google+. He suggested using fixed point math for all calculations, which resulted in a speed increase at around ten times what I initially had. This increase in speed almost makes it unneeded to have several levels of quality for present computers. His suggestions also included a few other tricks I was not aware of, which definitely will improve the next project I work on. You gotta love the internet.

JavaScript tunnel

Thursday 14.02.2013

I found an article describing how an old demo effect was implemented at Tunnel effect, and I wanted to try to implement this effect using HTML5 JavaScript.

JavaScript is not well suited for these type of effects, so I implemented several quality levels. The render quality can be changed in the top right corner. It is also possible to improve performance by resizing the browser window to a smaller size.

Go to Tunnel.

Added Commodore 64 Games

Saturday 02.02.2013

I've added some games that I made on my Commodore 64. I found these games on some old cassette tapes I found in a box. I decided to try to import them to my computer and digitally preserve them that way. In order to transfer the games I bought a USB cassette player. Each side of these cassette tapes hold 30 minutes of noise, and I had to run through these tapes in 1:1 speed using different methods for recording the sound. After trying several tools for converting autdio files into Commodore 64 application files, I got best results using WAV-PRG. This application takes the recorded audiofile, and outputs several PRG files, which can then be opened in an emulator. I've added the games I was able to extract as a downloadable archive. For playing these games, I recommend using The VICE Emulator.

To compare a cassette tape with the storage technologies of today, the transfer rate of the Commodore 64 cassette was roughly 300 bits/s. This gives an overwhelming 100kB per 30 minute cassette. The storage capacity could be improved to 1000kB using turbo tape software or other fast loaders. (source)

In addition to this, I added counts behind category and tech names, so that one can easily see how many titles are contained behind each link.

Online midlets

Sunday 27.01.2013

Lately I've been working on viewing the mobile games I've made on these pages. Games created for mobile Java, or MIDlets, are not easily runnable nowadays.

J2ME was originally formed through JSR 68 in 2002. The standard has later been abandoned, and nowadays one have to create applications for each target platform.

In order to run MIDlets one has to use an emulator. The most popular one is MicroEmulator. The emulation in this emulator is however not perfect. You do for example need to change your application somewhat for it to be runnable by the emulator.

After modifying the games so that they were runnable by the emulator I ran into problems regarding the audio. For some reason their implementation of the audio libraries only allow you to playback a unique sound once. There were also some problems regarding MIDI playback that stops after a few seconds. In addition there were some problems related to how loading of sounds are implemented, which results in crashes on load. Looking through their code I was not able to figure our how their implementation should be fixed.

What I ended up doing, was to use the reference implementation provided by Sun/Oracle. This implementation shows how to implement their interfaces. These implementations have some methods that reference native calls to the OS. I implemented all these native methods using the J2SE sound API. The end result was a fully working MIDP 2.0 sound API implementation.

Armed with this fully working implementation I patched the emulator. This resulted in an emulator that perfectly emulates the audio calls from the original games. This was the last piece of the puzzle, and I can therefore present these games on these pages.

My games originally needed only sampled and MIDI playback. The original API also defines functionality for playing tones. For the sake of completion, and for the fun of it, I also implemented this API. The biggest challenge here was to convert notes into their respective frequencies. By using a list of frequencies for each note, I managed to get a working formula. Using the reference implementation I didn't have to implement all the special operations, as this was already converted into a simple note for number of milliseconds datastructure. The use of blocks were for example converted into one long sequential sequence. The end result has some noticeable defects when the song turns into low frequent period, which almost sounds like an UFO buzzing away. There are also some clicks between notes sometimes. These errors are mostly related to rounding errors, and might be something I fix at a later date.

Pages updated

Monday 07.01.2013

These pages has received a facelift. The design of these pages has remained the same since late 2009, and I din't like how the look anymore. I therefore decided to take some time and update the design of these pages. I've also taken the time to update the design using modern techniques. The page design is for example completely responsive, which means that the same page will be able to serve content to all devices from the high-end desktop user to a mobile phone. This was realized using the Amazium framework.

During this process I've also updated the pages to use the latest version of the CakePHP framework. This framework has changed a lot since I've used it last, and I therefore had to change a lot of the backend code.

Take a look at the screenshots below to see how the page design has changed since the last design. The last image shows how the new pages look on a mobile device.

Motigon has been released

Friday 14.12.2012

The Motigon library has finally been released. We are pleased to finally be able to show the result of our hard work with all of you. In the last few weeks we've been busy writing documentation as well as updating the web pages with as much information we can provide.

The examples browser is available at http://www.motigon.com/examples/, with lots of examples on how to use the library. This browser will be extended with new examples as we add new features to the library, as well as based on feedback from our users.

All documentation is available at http://www.motigon.com/lib/documents.html. Here you will find both the JavaDoc for the full Java/GWT library as well as JsDoc for the JavaScript implementation. There is also a simple manual that walks you through the most used functionality of the default application setup.

If there is something missing in either the library or in the documentation, please don't hesitate to drop us a mail message at motigon@wis.no or thought the forum. We will be more than happy to help you with the problems you are facing using our library, if any.

Motify is dead, long live Motigon

Wednesday 21.11.2012

In the final stages of the development of our diagramming solution we have decided to change the name of the product. This decision was made because of a name conflict with an another commercial product, which our lawyers tells us is a bad thing. The new name of the product will be "Motigon", as you can see from the new blog title. The logo will still remain the same.

If you have been visiting our web pages lately, you might have seen that we've added a lot of new content. In addition to the general presentation we had earlier, we've also added a lot of documentation aimed at developers. We have also updated the previously mentioned API documentation for both the Java and JavaScript interfaces. Please have a look at our new pages at: http://www.motigon.com/

When the final details are ironed out, we are finally ready for a release of this product. If everything works out as planned, we are only a few days away from a public release of the library.

Using system clipboard from GWT

Tuesday 30.10.2012

The problem

There is no official support in the HTML standard to access the system clipboard. With that being said, there are a few tricks that can be utilized to work around this limitation. Most browsers support copy and paste operations only from text boxes or other editable fields. The content of these fields can be transferred to and from the system clipboard using the right-click menu or keyboard shortcuts.

The system clipboard will contain one or several versions of its content. The content could have a format of for example text, image data, html, xml and so on. Whenever the source application determines that a copied element could be used in different formats, multiple copies of the selected element in various formats will be placed on the clipboard. The application that reads the clipboard content could then fetch the content in the format it wants.

The exception

Internet Explorer is the only browser that support programmatic access to the system clipboard. When you run this operation in your code, you are greeted by one or two dialogs asking you as a user if it is OK for you that the running application should be allowed to access the system clipboard. When you have allowed clipboard access for this application, you will not be asked again until you close the browser window. You could of course add the website in question to your list of trusted sites, and set the security level for trusted sites to low. Or even change the specific setting. You would then open yourself to a world of security holes, which makes this solution less than recommended. To make matters even worse, IE version 6 and below will not even ask before accessing the system clipboard, opening for even more security holes.

Google Docs

Lets start by looking at how Google solved this for Google Docs. In the article "Google Docs and Clipboard Access", some of the issues found while implementing support for copy and paste for Google Docs are described. When writing a document in Google Docs (or Blogger), you are writing the text within a large editable area. This area allows keyboard shortcuts because the browser natively support copy and paste operations using keyboard shortcuts within a text area or text field handled by the browser. Using the browser context menu to do copy and paste will also work in such a field. A customized right-click menu or a menu bar will not be able to do these operations for most browsers.

So how does Google Docs handle menu based clipboard access from the different types of browsers? IE gets programmatic access after showing one or two warning dialogs. Firefox gets a dialog that asks the user to please use keyboard shortcuts. Chrome gets a message telling the user to install their Chrome specific Google Drive application. When a company as large as Google is not able to solve this problem, the problem has to be quite complex.

Some possible solutions

These problems show that we cannot rely on stock JavaScript alone to access the system clipboard. What assisted alternatives do we have to solve this problem?

Internet Explorer clipboardData

As mentioned earlier, IE allows you to access the system clipboard directly. Even though you are greeted with some annoying warning dialogs when your application tries to access these API's, this approach gives full access to the system clipboard. This is realized using the IE version of the clipboardData object.

Data is written to the clipboard in this way:


var success = window.clipboardData.setData("Text", "The text");

Data could then be read using something like this:


var data = window.clipboardData.getData("Text");

This approach would work if you are targeting IE alone, or if you want to create a separate implementation for each browser. We want an implementation that supports several browsers, preferably using a similar method for all major browsers.

Adobe Flash

There are several libraries for accessing the clipboard using Flash. For the sake of an example, I will be using one called Zero Clipboard. When using a Flash helper you should be aware that Flash imposes a few restrictions on your use of the clipboard. Because of how the Flash sandbox is implemented you can only push data, while pulling is not allowed. Using Flash Player version 9 or lower you could push data at any time. From Flash Player version 10 an additional restriction were added. Here you can only paste data when the user does a user interaction within the Flash movie, like a mouse click or a button press.

Data can be pushed to the system clipboard using something like this:


var clip = new ZeroClipboard.Client();
clip.setText("Copy me!");

As this solution is a one-way street, it might not solve your problem. Because of how the sandbox were further closed for the version 10 release, these added restriction might also be a hinder for your usecase. One might trick the Flash runtime by setting focus to the element when the user initiates a keystroke or a mouse click. Zero Clipboard solves this problem by placing the movie clip on top of the element in question. These methods are simply bad workarounds, and should only be considered as a last resort.

An another thing to consider is that Flash support is being dropped in the Apple land after their hate campaign against the use of Flash. Adobe tries to defend their position, but now that Flash support is also dropped from Android devices, we can safely say that we should leave this platform behind us.

Microsoft Silverlight

Support for accessing the system clipboard was added in Silverlight version 4. The Clipboard class lets you read and write to the clipboard. Version 5 does however add restrictions similar to the Flash plugin, in that these operations must be the result of user interaction. As long as these methods are called as the result of a keyboard press event or a mouse press event, access to these functions are allowed. It should also be noted that the Windows Phone version of Silverlight only allows you to set the content of the system clipboard. It should also be noted that Microsoft themselves now seems to abandon Silverlight. It is therefore preferable to not rely on Silverlight for clipboard access.

Java applet

Pure Java applications is able to access the native clipboard perfectly. This can be seen to its full effect in full-blown Java-driven applications like Netbeans and Eclipse. Applets used to work in the same way. When Oracle released Java Plugin 1.6.0_24 in February 2011, they closed access to the system clipboard as a way to stop a security hole. What this means in practice, is that you can copy and paste data between Java applets as you would expect them to. The data you copy does however never reach the system clipboard. The Java runtime uses an internal clipboard that will only feed applets with data.

This restriction can be overridden in several ways. The simplest way is to modify your local policy file so that applets have access to the system clipboard. This requires that your end users are relatively tech-savvy, and that they will accept to do a change like this, knowing that it will also affect how applets are handled on other web pages. This is highly unwanted, and one would really like a better solution.

An another way to override the clipboard sandbox is to use a verified certificate. Such a certificate can be bought from sources like Verisign / Symantec. These certificates cost as of today $499. These certificates open the sandbox completely and lets you access everything a desktop Java application could.

With these quirks out of the way, we remain with the problem of communicating with the applet from JavaScript. Communicating with JavaScript from a Java applet is relatively easy, but problems start to occur when we want the communication to be initiated from JavaScript.

The first version of JavaScript initiated communication was called LiveConnect. This technique was implemented as part of the Netscape 4 browser in June 1997. This communication still works in Firefox and Opera, but Chrome has dropped support, while IE has never supported it.

LiveConnect has been surpassed by NPRuntime. This method is supported by all major browser with the exception of IE. On their own pages the Chrome developers does not recommend the use of NPAPI plugins, as they can not easily be fit within a sandbox, and is therefore a security threat.

With all these problems we can safely say that a Java applet is not a recommended solution. With problems like sandbox restrictions and communication problems, as well as browser incompatibilities, it will be really hard to make a solution that works in all situations. It should also be noted that the startup of a Java applet requires a few seconds the first time. The next time you visit a page with the same applet, it start up immediately. This startup delay will be repeated when you close the browser.

execCommand

The execCommand method lets you execute internal functionality of the browser. These calls could for example be used to create a rich-text editor.

For copying the text, something like this would suffice:


function copy() {
  var area = document.getElementById("area1");
  area.focus();
  area.select();
  document.execCommand("copy", false, null);
}

For pasting the content back into the browser, this method could be run:


function paste() {
  var area = document.getElementById("area2");
  area.focus();
  area.select();
  document.execCommand("paste", false, null);
}

Note that the two last parameters are required in Firefox and Opera, while they are optional in IE, Chrome and Safari.

The result and availability of these calls vary widely between browsers. For our use we need the "copy" and "paste" commands. These two commands are available in IE and Safari only, while the other commands are available for most browsers. When running these commands on these browsers the selected text will be copied to the clipboard for the "copy" command, while the "paste" command insert the clipboard content within the component that currently has focus.

Because these commands works in only a few browsers, they aren't too useful for our use. For the browsers that this method works with, the previously mentioned methods that do not require an additional text box is preferred.

Mozilla XUL

Firefox has a language for building user interfaces called XUL. Applications built using XUL can be compiled to native applications, creating truly cross-plattform applications. Within XUL there are functionality to access the system clipboard. Accessing this data could be a lot of pain though.

Accessing the clipboard using XUL in the browser is not available by default. There are four scenarios where accessing the system clipboard is possible:

  • Copy the web pages to your local machine and run it from there, which hardly can be called a modern web.
  • Change some obscure setting within the browser, which in turn excludes the lesser tech-savvy endusers.
  • Activate the security setting using JavaScript. This makes a dialog pop up in the browser asking the user for permission to do so.
  • Digitally sign the JavaScript.

With these options covered, it should also be mentioned that XUL will only work in Firefox. If you want a truly cross-platform solution, this approach will not help you.

Clipboard events

There are six events available for clipboard events. One before and one after event of each of the "copy", "cut" and "paste" operations. How these events work between the different browsers vary wildly. There is also a bug registered for WebKit, which makes it impossible to write to the clipboard events using Chrome and Safari. The official standard also mention security risks with these events, which might be the reason why this bug has been lingering since 2008.

Because we want these operations to work both ways, we need to find an another approach that works both ways in all browsers.

Hidden textarea

An another method is to use a hidden textarea trick. This involves positioning a textarea element outside of the view of the user. When the user then presses any of the keyboard shortcuts for clipboard operations, this textarea will get focus and be selected. The browser will then either copy the text out of this field into the system clipboard or place new text within it. Either way we trick the browser into using the content of this hidden textarea for all copy and paste operations. This is the same trick that is used within CkEditor and TinyMCE rich-text editors.

One might think that it would be easier to hide the textarea within the visible browser area of the screen using display none or similar. This approach would stop browsers from pasting to the textarea, as the browser sandbox will prevent you from pasting to hidden fields. Positioning the textarea outside of the view will also in practice hide the field from view, but this fact is overlooked by the browser.

This approach seems to work fine in all browsers. Although a bit cheaty, this method will allow you to copy and paste text using the keyboard shortcuts.

The implementation

When looking at the various alternatives, I decided to try to implement the hidden textarea trick for the Motify application. The other methods works only for certain browsers, while some only works in very special conditions. This method seems to solve the problem in a somewhat elegant way that is totally transparent to the end user. We've implemented the application using Google's GWT framework, and I've therefore implemented this functionality using GWT concepts.

Step one is to add a textbox hidden out of view. I started this implemention using a text field, but this component do not preserve line changes. All lines were simply merged into one line without newlines. This problem does not occur when using a textarea.


this.textBox = new TextArea();
this.textBox.getElement().getStyle().setPosition(Position.ABSOLUTE);
this.textBox.getElement().getStyle().setZIndex(100);
this.textBox.getElement().getStyle().setLeft(-1000, Unit.PX);
RootPanel.get().add(this.textBox);

Events need to be processed before all other event handlers. We therefore attach a native preview handler.


Event.addNativePreviewHandler(this);

When handling the native events, we restrict event handling to keyboard down events.


@Override
public final void onPreviewNativeEvent(final NativePreviewEvent event) {
    final NativeEvent nativeEvent = event.getNativeEvent();
    switch (Event.getTypeInt(nativeEvent.getType())) {
    case Event.ONKEYDOWN:
        this.onKeyDown(nativeEvent);
        break;

    default:
        break;
    }
}

Key events are then restricted to copy and paste shortcuts:


private void onKeyDown(final NativeEvent event) {
    if (!event.getCtrlKey() || !this.containerRenderer.hasFocus()) {
        return;
    }

    if (event.getKeyCode() == ExtendedKeyCodes.KEY_C
            || event.getKeyCode() == ExtendedKeyCodes.KEY_X) {
        this.textBox.setText(this.getContent());
        this.textBox.setFocus(true);
        this.textBox.selectAll();

        Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
            @Override
            public boolean execute() {
                TextBoxClipboardHandler.this.containerRenderer.setFocus();
                return false;
            }
        }, INTERCEPT_DELAY);
    }

    if (event.getKeyCode() == ExtendedKeyCodes.KEY_V) {
        this.textBox.setText("");
        this.textBox.setFocus(true);
        this.lastInterceptedPaste = System.currentTimeMillis();
    }
}

We start by checking if the ctrl was used, as we want to check only key events with this modifier. The containerRenderer variable holds a reference to the component where we want to handle copy and paste keyboard events. In our case this is the area used for drawing diagrams. If this component does not hold the current focus, we will not process keyboard events.

Copy and cut keyboard shortcuts (C and X) starts by fetching the content to be placed on the system clipboard. This text is then pushed to the text box, as well as receiving the current focus. The full content of this text box is then selected, ready to be placed on the system clipboard. At this point the textual content assigned to the field will be fetched by the browser and placed on the user clipboard. The final step is to assign focus back to the drawing area. This needs to be done after the browser has moved the content from the text box over to the system clipboard. The delay chosen for my implementation was 100ms. This delay seems to be enough for all browsers to do their needed job. The difference between copy and cut operations are handled outside of this class, where a key down handler removes drawing content when using the keyboard shortcut for cut.

The paste keyboard shortcut (V) starts by removing the current content of the text box. Focus is then assigned to the field, and we remember when we last started a paste interception. The rest of the paste handling is handled outside of the class, which then returns to the paste method within this class.


@Override
public final void paste() {
    if (Math.abs(System.currentTimeMillis()
            - this.lastInterceptedPaste) < INTERCEPT_LIMIT) {
        Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
            @Override
            public boolean execute() {
                TextBoxClipboardHandler.this.setContent(
                        TextBoxClipboardHandler.this.textBox.getText());
                TextBoxClipboardHandler.this.containerRenderer.setFocus();
                return false;
            }
        }, INTERCEPT_DELAY);
    } else {
        this.internalClipboardHandler.paste();
    }
}

We start paste processing by checking if the time difference between the current time and the last interception is less than the interception limit. This limit is also set to 100ms. The reason why we check for this, is that we want to set focus back to the drawing area only if the paste event was initiated using a keyboard shortcut when focus is assigned to the drawing area. Pressing the copy and paste functions from the toolbar should not change the focus, but instead perform an internal paste operation. When using the keyboard shortcut, the text box will contain content that we then will process and insert into the drawing. The drawing container will then receive the focus.

The paste box as positioned within the Motify application

When we look at the application as it is visualized using the tilt function of Firefox, we can see that the text area is placed to the far left. In this image the text area is currently selected, and is therefore shown in blue on the far left. As we can see, the rest of the application on the right side is completely unobfuscated by the text area.

This solution is currently in place for the Motify application, and works completely transparent for the end user. A normal user expects web applications to behave in the exact same way as desktop applications, and finds it a hassle if the application does not support rudimentary functionality like copy and paste in the way that they expect. We did support internal copy and paste within the same drawing earlier, but with this implementation in place we can now easily copy elements between drawings.

First published at:
http://infposs.blogspot.no/2012/10/using-system-clipboard-from-gwt.html

Motify examples

Wednesday 03.10.2012

As we approach the release of the Motify library we want to show you some of what we have been working on. These examples give a small look into how the final product will be like. These examples are not yet in its final form, and might therefore contain bugs and lack some features. When we release the final version of the library, these examples will be fully polished and contain all the functionality they should have.

Complete example

This example show what a developer can do using the GWT version of the library. This example uses the default GWT interface. While this interface might be good enough for most uses, the interface is completely customizable from code. http://www.motigon.com/demo/

Example browser

The example browser contains several demos that shows how to use various elements of the library. All these examples contain full source code so that it's easy to find an example for most scenarios. The browser will be extended as more features are added to the library. http://www.motigon.no/examples/

JavaDoc

Complete documentation for the Java / GWT library. Here you will find how the library is organized as well as how each method is intended to be called. Details on how parts of the library works with each other can be found in the example browser. http://www.motigon.no/javadoc/

JsDoc

Complete documentation for the JavaScript library. This documentation tries to follow the format of the documentation generated by the JsDoc project. Because this documentation is generated from Java sources that has been exposed as JavaScript, parts of this documentation will not appear quite like its pure JavaScript counterpart. http://www.motigon.no/jsdoc/

2017

2016

2014

2013

2012

2011

2010

2009

2007

2005

2004

2003

2002

2001