Commits

Rich Manalang [Atlassian]  committed b5ab0cd Merge

updating fork

Change-Id: I605b5ae6a7c658daa010350637d0187c66ec5713

  • Participants
  • Parent commits 04f3726, 6b7217e

Comments (0)

Files changed (162)

File apputils/src/main/java/com/atlassian/labs/remoteapps/apputils/OAuthContext.java

 
     public String validateRequest(HttpServletRequest req) throws ServletException
     {
-        OAuthMessage message = OAuthServlet.getMessage(req, null);
+        URI originalUri = URI.create(req.getRequestURI());
+        String url = getLocalBaseUrl() + originalUri.getPath();
+        if (originalUri.getFragment() != null)
+        {
+            url += "#" + originalUri.getFragment();
+        }
+        OAuthMessage message = OAuthServlet.getMessage(req, url);
         return validateAndExtractKey(message);
     }
 

File bin/release-prepare.sh

 echo "Updating source"
 git pull origin master
 
-echo "Checking out $GIT_SHA1..."
+echo "Checking out $GIT_SHA1.  If this fails, it is due to this build being a merge."
 git checkout $GIT_SHA1

File plugin/pom.xml

             <version>20090531</version>
         </dependency>
         <dependency>
+            <groupId>net.oauth.core</groupId>
+            <artifactId>oauth</artifactId>
+            <version>20090531</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>com.atlassian.labs</groupId>
             <artifactId>jira4-compat</artifactId>
             <version>0.6.1</version>
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient-cache</artifactId>
-            <version>4.1.2</version>
+            <version>4.2-beta1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpasyncclient</artifactId>
+            <version>4.0-beta1</version>
         </dependency>
 
         <dependency>
         </dependency>
 
         <dependency>
+            <groupId>cc.plural</groupId>
+            <artifactId>jsonij</artifactId>
+            <version>0.2.11</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
             <groupId>org.eclipse.jetty</groupId>
             <artifactId>jetty-servlet</artifactId>
             <version>7.1.6.v20100715</version>
                 <version>1.5.8</version>
                 <scope>provided</scope>
             </dependency>
-            <dependency>
-                <groupId>net.oauth.core</groupId>
-                <artifactId>oauth</artifactId>
-                <version>20090531</version>
-                <scope>provided</scope>
-            </dependency>
         </dependencies>
     </dependencyManagement>
 
                         </Import-Package>
                         <DynamicImport-Package>com.atlassian.labs.speakeasy.*</DynamicImport-Package>
                         <Export-Package>
+                            com.atlassian.labs.remoteapps.modules.confluence,
                             com.atlassian.labs.remoteapps.modules.permissions.scope,
                             com.atlassian.labs.remoteapps.modules.page.jira,
                             com.atlassian.labs.remoteapps.descriptor.external,

File plugin/src/main/java/com/atlassian/labs/remoteapps/DefaultRemoteAppsService.java

 import com.atlassian.labs.remoteapps.api.PermissionDeniedException;
 import com.atlassian.labs.remoteapps.api.RemoteAppsService;
 import com.atlassian.labs.remoteapps.installer.RemoteAppInstaller;
+import com.atlassian.labs.remoteapps.installer.SchemeDelegatingRemoteAppInstaller;
 import com.atlassian.labs.remoteapps.util.BundleUtil;
 import com.atlassian.plugin.PluginAccessor;
 import com.atlassian.plugin.PluginController;
  */
 public class DefaultRemoteAppsService implements RemoteAppsService
 {
-    private final RemoteAppInstaller remoteAppInstaller;
+    private final SchemeDelegatingRemoteAppInstaller remoteAppInstaller;
     private final UserManager userManager;
     private final BundleContext bundleContext;
     private final PermissionManager permissionManager;
     private final PluginAccessor pluginAccessor;
     private static final Logger log = LoggerFactory.getLogger(DefaultRemoteAppsService.class);
 
-    public DefaultRemoteAppsService(RemoteAppInstaller remoteAppInstaller, UserManager userManager,
+    public DefaultRemoteAppsService(SchemeDelegatingRemoteAppInstaller remoteAppInstaller, UserManager userManager,
             BundleContext bundleContext, PermissionManager permissionManager,
             PluginController pluginController,
             PluginAccessor pluginAccessor)
     {
         validateCanInstall(username);
 
+        URI parsedRegistrationUri;
         try
         {
-            new URI(registrationUrl);
+            parsedRegistrationUri = new URI(registrationUrl);
         }
         catch (URISyntaxException e)
         {
         }
         try
         {
-            String appKey = remoteAppInstaller.install(username, registrationUrl, registrationSecret,
+            String appKey = remoteAppInstaller.install(username, parsedRegistrationUri, registrationSecret,
                     stripUnknownModules, new RemoteAppInstaller.KeyValidator()
                {
                    @Override

File plugin/src/main/java/com/atlassian/labs/remoteapps/DescriptorValidator.java

 import javax.xml.validation.Validator;
 import java.io.IOException;
 import java.io.StringReader;
+import java.net.URI;
 import java.net.URL;
 import java.util.*;
 
         this.applicationProperties = applicationProperties;
     }
 
-    public Document parseAndValidate(String url, String descriptorXml)
+    public Document parseAndValidate(URI url, String descriptorXml)
     {
         SAXReader reader = XmlUtils.createSecureSaxReader();
         try
         {
             InputSource source = new InputSource(new StringReader(descriptorXml));
-            source.setSystemId(url);
+            source.setSystemId(url.toString());
             source.setEncoding("UTF-8");
             Document document = reader.read(source);
             document.accept(new NamespaceCleaner());
         }
     }
 
-    public void validate(String url, Document document)
+    public void validate(URI url, Document document)
     {
         SchemaFactory schemaFactory = SchemaFactory
                 .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
         try
         {
             DocumentSource source = new DocumentSource(document);
-            source.setSystemId(url);
+            source.setSystemId(url.toString());
             validator.validate(source);
         }
         catch (SAXException e)
             addSchemaDocumentation(module, generator);
         }
 
-        return printDocument(root.getDocument());
+        return printNode(root.getDocument());
     }
 
     private void processIncludes(Document doc, Set<String> includedDocIds)
 
     private void insertAvailableLinkContextParams(Document includeDoc, Map<String, String> linkContextParams)
     {
-        Element restriction = (Element) includeDoc.selectSingleNode("/xs:schema/xs:simpleType[@name='LinkContextParamNameType']/xs:restriction");
+        Element restriction = (Element) includeDoc.selectSingleNode("/xs:schema/xs:simpleType[@name='LinkContextParameterNameType']/xs:restriction");
         if (restriction != null)
         {
             for (Map.Entry<String, String> entry : linkContextParams.entrySet())

File plugin/src/main/java/com/atlassian/labs/remoteapps/OAuthLinkManager.java

 import com.atlassian.oauth.consumer.ConsumerService;
 import com.atlassian.oauth.serviceprovider.ServiceProviderConsumerStore;
 import com.atlassian.sal.api.user.UserManager;
+import com.google.common.base.Function;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
 import net.oauth.*;
 import net.oauth.signature.RSA_SHA1;
 import org.apache.http.HttpHeaders;
     private final AuthenticationConfigurationManager authenticationConfigurationManager;
     private final ApplicationLinkService applicationLinkService;
     private final ConsumerService consumerService;
-    private final UserManager userManager;
     private final OAuthValidator oauthValidator;
 
     @Autowired
     public OAuthLinkManager(ServiceProviderConsumerStore serviceProviderConsumerStore,
                             AuthenticationConfigurationManager authenticationConfigurationManager,
                             ApplicationLinkService applicationLinkService,
-                            ConsumerService consumerService,
-                            UserManager userManager)
+                            ConsumerService consumerService)
     {
         this.serviceProviderConsumerStore = serviceProviderConsumerStore;
         this.authenticationConfigurationManager = authenticationConfigurationManager;
         this.applicationLinkService = applicationLinkService;
         this.consumerService = consumerService;
-        this.userManager = userManager;
         this.oauthValidator = new SimpleOAuthValidator();
     }
 
         }
     }
 
-    public void sign(HttpRequestBase httpMessage, ApplicationLink link, String url, String userName, Map<String, List<String>> originalParams)
+    public void sign(HttpRequestBase httpMessage, ApplicationLink link, String url, Map<String, List<String>> originalParams)
     {
-        OAuthMessage message = sign(link, httpMessage.getMethod(), url, userName, originalParams);
+        OAuthMessage message = sign(link, httpMessage.getMethod(), url, originalParams);
         if (message != null)
         {
             try
 
     public List<Map.Entry<String, String>> signAsParameters(ApplicationLink link, String method, String url, Map<String, List<String>> originalParams)
     {
-        OAuthMessage message = sign(link, method, url, userManager.getRemoteUsername(), originalParams);
+        OAuthMessage message = sign(link, method, url, originalParams);
         if (message != null)
         {
             try
         }
         else
         {
-            return newArrayList(Collections.<String,String>emptyMap().entrySet());
+            return newArrayList(Maps.transformValues(originalParams, new Function<List<String>, String>()
+            {
+                @Override
+                public String apply(List<String> strings)
+                {
+                    // TODO: Doesn't handle multiple values with the same param name
+                    return strings.get(0);
+                }
+            }).entrySet());
         }
     }
 
-    private OAuthMessage sign(ApplicationLink link, String method, String url, String userName, Map<String, List<String>> originalParams)
+    private OAuthMessage sign(ApplicationLink link, String method, String url, Map<String, List<String>> originalParams)
     {
         Map<String,List<String>> params = newHashMap(originalParams);
         Consumer self = consumerService.getConsumer();
         params.put(OAuth.OAUTH_CONSUMER_KEY, singletonList(self.getKey()));
-        if (userName != null)
-        {
-            params.put("user_id", singletonList(userName));
-        }
         if (log.isDebugEnabled())
         {
             dumpParamsToSign(params);

File plugin/src/main/java/com/atlassian/labs/remoteapps/PermissionManager.java

 import com.atlassian.applinks.api.ApplicationLink;
 import com.atlassian.applinks.api.ApplicationLinkService;
 import com.atlassian.applinks.api.ApplicationType;
+import com.atlassian.labs.remoteapps.modules.applinks.RemoteAppApplicationType;
 import com.atlassian.labs.remoteapps.modules.permissions.scope.ApiScope;
 import com.atlassian.labs.remoteapps.settings.SettingsManager;
 import com.atlassian.labs.remoteapps.util.ServletUtils;
             }
         }
 
-        ApplicationLink link = linkManager.getLinkForOAuthClientKey(clientKey);
+        ApplicationLink link = getLinkForClientKey(clientKey);
         if (link == null)
         {
             return false;
         return false;
     }
 
+    public ApplicationLink getLinkForClientKey(String clientKey)
+    {
+        ApplicationLink link = linkManager.getLinkForOAuthClientKey(clientKey);
+        if (link == null)
+        {
+            // fallback to checking all app links
+            for (ApplicationLink aLink : applicationLinkService.getApplicationLinks())
+            {
+                ApplicationType type = aLink.getType();
+                if (type instanceof RemoteAppApplicationType)
+                {
+                    RemoteAppApplicationType raType = (RemoteAppApplicationType) type;
+                    if (clientKey.equals(raType.getId().get()))
+                    {
+                        link = aLink;
+                    }
+                }
+            }
+        }
+        return link;
+    }
+
     public boolean canInstallRemoteApps(String username)
     {
         return username != null &&

File plugin/src/main/java/com/atlassian/labs/remoteapps/RetrievalTimeoutException.java

+package com.atlassian.labs.remoteapps;
+
+/**
+ * If the content cannot be retrieved in the given time
+ */
+public class RetrievalTimeoutException extends ContentRetrievalException
+{
+    public RetrievalTimeoutException()
+    {
+        super();
+    }
+
+    public RetrievalTimeoutException(String message)
+    {
+        super(message);
+    }
+
+    public RetrievalTimeoutException(String message, Throwable cause)
+    {
+        super(message, cause);
+    }
+
+    public RetrievalTimeoutException(Throwable cause)
+    {
+        super(cause);
+    }
+}

File plugin/src/main/java/com/atlassian/labs/remoteapps/installer/DefaultRemoteAppInstaller.java

 package com.atlassian.labs.remoteapps.installer;
 
-import com.atlassian.event.api.EventListener;
 import com.atlassian.event.api.EventPublisher;
-import com.atlassian.jira.plugin.searchrequestview.SearchRequestView;
 import com.atlassian.labs.remoteapps.DescriptorValidator;
 import com.atlassian.labs.remoteapps.ModuleGeneratorManager;
 import com.atlassian.labs.remoteapps.OAuthLinkManager;
 import com.atlassian.labs.remoteapps.api.InstallationFailedException;
 import com.atlassian.labs.remoteapps.api.PermissionDeniedException;
 import com.atlassian.labs.remoteapps.event.RemoteAppInstalledEvent;
-import com.atlassian.labs.remoteapps.event.RemoteAppStartFailedEvent;
-import com.atlassian.labs.remoteapps.event.RemoteAppStartedEvent;
 import com.atlassian.labs.remoteapps.modules.external.RemoteModuleGenerator;
 import com.atlassian.labs.remoteapps.modules.page.jira.JiraProfileTabModuleGenerator;
 import com.atlassian.labs.remoteapps.util.zip.ZipBuilder;
 import com.atlassian.plugin.*;
 import com.atlassian.sal.api.ApplicationProperties;
 import com.atlassian.sal.api.net.*;
-import com.google.common.collect.ImmutableMap;
 import org.dom4j.Document;
 import org.dom4j.DocumentHelper;
 import org.dom4j.Element;
 @Component
 public class DefaultRemoteAppInstaller implements RemoteAppInstaller
 {
-    public static final int INSTALLATION_TIMEOUT = 10;
     private final ConsumerService consumerService;
     private final RequestFactory requestFactory;
     private final PluginController pluginController;
     private final ApplicationProperties applicationProperties;
-    private final ModuleGeneratorManager moduleGeneratorManager;
-    private final EventPublisher eventPublisher;
     private final DescriptorValidator descriptorValidator;
     private final PluginAccessor pluginAccessor;
     private final OAuthLinkManager oAuthLinkManager;
     private final FormatConverter formatConverter;
-    private final BundleContext bundleContext;
+    private final InstallerHelper installerHelper;
 
+    private final BundleContext bundleContext;
     private static final Logger log = LoggerFactory.getLogger(
             DefaultRemoteAppInstaller.class);
 
             RequestFactory requestFactory,
             PluginController pluginController,
             ApplicationProperties applicationProperties,
-            ModuleGeneratorManager moduleGeneratorManager,
-            EventPublisher eventPublisher,
             DescriptorValidator descriptorValidator,
             PluginAccessor pluginAccessor,
             OAuthLinkManager oAuthLinkManager, FormatConverter formatConverter,
-            BundleContext bundleContext)
+            InstallerHelper installerHelper, BundleContext bundleContext)
     {
         this.consumerService = consumerService;
         this.requestFactory = requestFactory;
         this.pluginController = pluginController;
         this.applicationProperties = applicationProperties;
-        this.moduleGeneratorManager = moduleGeneratorManager;
-        this.eventPublisher = eventPublisher;
         this.descriptorValidator = descriptorValidator;
         this.pluginAccessor = pluginAccessor;
         this.oAuthLinkManager = oAuthLinkManager;
         this.formatConverter = formatConverter;
+        this.installerHelper = installerHelper;
         this.bundleContext = bundleContext;
     }
 
     @Override
-    public String install(final String username, final String registrationUrl,
+    public String install(final String username, final URI registrationUrl,
             String registrationSecret, final boolean stripUnknownModules, final KeyValidator keyValidator) throws
                                                                         PermissionDeniedException
     {
         </ul>
          */
         final Consumer consumer = consumerService.getConsumer();
-        final URI registrationUri = URI.create(
-                encodeGetUrl(registrationUrl, new HashMap<String,String>() {{
+        final URI registrationUriWithParams = URI.create(
+                encodeGetUrl(registrationUrl.toString(), new HashMap<String,String>() {{
                     put("key", consumer.getKey());
                     put("publicKey", RSAKeys.toPemEncoding(consumer.getPublicKey()));
                     put("serverVersion", applicationProperties.getBuildNumber());
 
         log.info("Retrieving descriptor from '{}' by user '{}'", registrationUrl, username);
         Request request = requestFactory.createRequest(Request.MethodType.GET,
-                registrationUri.toString());
+                registrationUriWithParams.toString());
 
         /*!
         The registration secret is passed via the Authorization header using a custom scheme called
                             String descriptorText = response.getResponseBodyAsString();
                             String contentType = response.getHeader("Content-Type");
                             Document document = formatConverter.toDocument(
-                                    registrationUrl, contentType, descriptorText);
+                                    registrationUrl.toString(), contentType, descriptorText);
                             
                            /*!
                            If the 'stripUnknownModules' flag is set to true, all unknown modules
                             */
                             if (stripUnknownModules)
                             {
-                                detachUnknownModuleElements(document);
+                                installerHelper.detachUnknownModuleElements(document);
                             }
 
                            /*!
                              an administrator</li>
                            </ul>
                             */
-                            final Properties i18nMessages = new Properties();
-                            try
-                            {
-                                moduleGeneratorManager.getApplicationTypeModuleGenerator()
-                                        .validate(root, registrationUrl, username);
-
-                                ValidateModuleHandler moduleValidator = new ValidateModuleHandler(
-                                        registrationUrl,
-                                        username,
-                                        i18nMessages,
-                                        pluginKey);
-                                moduleGeneratorManager.processDescriptor(root, moduleValidator);
-                            }
-                            catch (PluginParseException ex)
-                            {
-                                throw new InstallationFailedException(
-                                        "Validation of the descriptor failed: " + ex.getMessage(),
-                                        ex);
-                            }
+                            final Properties i18nMessages = installerHelper
+                                    .validateAndGenerateMessages(root, pluginKey, registrationUrl,
+                                            username);
 
                             /*!
                             Finally, the descriptor XML is transformed into an Atlassian OSGi
                              contents of the plugin descriptor are derived from the remote app
                              descriptor.
                             */
-                            Document pluginXml = generatePluginDescriptor(username,
+                            Document pluginXml = installerHelper.generatePluginDescriptor(username,
                                     registrationUrl, document);
 
 
                             4. i18n.properties - An internationalization properties file containing
                                keys extracted out of the app descriptor XML.
                              */
-                            JarPluginArtifact jar = createJarPluginArtifact(pluginKey,
-                                    registrationUri.getHost(), pluginXml, document, i18nMessages);
+                            JarPluginArtifact jar = installerHelper.createJarPluginArtifact(
+                                    pluginKey,
+                                    registrationUrl.getHost(), pluginXml, document, i18nMessages);
 
                             /*!
                             The registration process should only return once the Remote App has
                             successfully installed and started.
                             */
-                            final CountDownLatch latch = new CountDownLatch(1);
-                            StartedListener startListener = new StartedListener(pluginKey, latch);
-                            eventPublisher.register(startListener);
-
-
-                            try
-                            {
-                                pluginController.installPlugins(jar);
-
-
-                                if (!latch.await(INSTALLATION_TIMEOUT, TimeUnit.SECONDS))
-                                {
-                                    Exception cause = startListener.getFailedCause();
-                                    if (cause != null)
-                                    {
-                                        log.info("Remote app '{}' was not started successfully and is "
-                                                + "disabled due to: {}", pluginKey,
-                                                cause);
-                                        throw new InstallationFailedException("Error starting app: "
-                                                + cause.getMessage(),
-                                                cause);
-                                    }
-                                    else
-                                    {
-                                        log.info("Remote app '{}' was not started successfully in "
-                                                + "the expected {} seconds.", pluginKey, INSTALLATION_TIMEOUT);
-                                        throw new InstallationFailedException("Timeout starting app");
-                                    }
-                                }
-                            }
-                            catch (InterruptedException e)
-                            {
-                                // ignore
-                            }
-                            finally
-                            {
-                                eventPublisher.unregister(startListener);
-                            }
-
-                            log.info("Registered app '{}' by '{}'", pluginKey, username);
-
-                            eventPublisher.publish(new RemoteAppInstalledEvent(pluginKey));
+                            installerHelper.installRemoteAppPlugin(username, pluginKey, jar);
 
                             return pluginKey;
                         }
         }
         /*!-helper methods */
     }
-
-    private void detachUnknownModuleElements(Document document)
-    {
-        Set<String> validModuleTypes = moduleGeneratorManager
-                .getModuleGeneratorKeys();
-        for (Element child : (List<Element>)document.getRootElement().elements())
-        {
-            if (!validModuleTypes.contains(child.getName()))
-            {
-                log.debug("Stripping unknown module '{}'", child.getName());
-                child.detach();
-            }
-        }
-    }
-
-    private JarPluginArtifact createJarPluginArtifact(final String pluginKey,
-            String host, final Document pluginXml, final Document appXml, final Properties props)
-    {
-        return new JarPluginArtifact(
-                ZipBuilder.buildZip("install-" + host, new ZipHandler()
-                {
-                    @Override
-                    public void build(ZipBuilder builder) throws IOException
-                    {
-                        attachResources(pluginKey, props, pluginXml, builder);
-                        builder.addFile("atlassian-plugin.xml", pluginXml);
-                        builder.addFile("META-INF/spring/remoteapps-loader.xml", getClass().getResourceAsStream("remoteapps-loader.xml"));
-                        builder.addFile("atlassian-remote-app.xml", appXml);
-                    }
-                }));
-    }
-
-    private static void attachResources(String pluginKey, Properties props,
-            Document pluginXml, ZipBuilder builder
-    ) throws IOException
-    {
-        final StringWriter writer = new StringWriter();
-        try
-        {
-            props.store(writer, "");
-        }
-        catch (IOException e)
-        {
-            // shouldn't happen
-            throw new RuntimeException(e);
-        }
-
-        pluginXml.getRootElement().addElement("resource")
-                .addAttribute("type", "i18n")
-                .addAttribute("name", "i18n")
-                .addAttribute("location", pluginKey.hashCode() + ".i18n");
-
-        builder.addFile(pluginKey.hashCode() + "/i18n.properties",
-                writer.toString());
-    }
-
-    private Document generatePluginDescriptor(String username,
-            String registrationUrl, Document doc)
-    {
-        Element oldRoot = doc.getRootElement();
-
-        final Element plugin = DocumentHelper.createElement("atlassian-plugin");
-        plugin.addAttribute("plugins-version", "2");
-        plugin.addAttribute("key", getRequiredAttribute(oldRoot, "key"));
-        plugin.addAttribute("name", calculatePluginName(getRequiredAttribute(oldRoot, "name")));
-        Element info = plugin.addElement("plugin-info");
-        info.addElement("version").setText(
-                getRequiredAttribute(oldRoot, "version"));
-
-        moduleGeneratorManager.processDescriptor(oldRoot,
-                new ModuleGeneratorManager.ModuleHandler()
-                {
-                    @Override
-                    public void handle(
-                            Element element,
-                            RemoteModuleGenerator generator)
-                    {
-                        generator.generatePluginDescriptor(
-                                element,
-                                plugin);
-                    }
-                });
-
-        if (oldRoot.element("vendor") != null)
-        {
-            info.add(oldRoot.element("vendor").detach());
-        }
-        Element instructions = info.addElement("bundle-instructions");
-        instructions
-                .addElement("Import-Package")
-                .setText(
-                        JiraProfileTabModuleGenerator.class.getPackage().getName() +
-                                ";resolution:=optional," +
-                                "com.atlassian.jira.plugin.searchrequestview;resolution:=optional," +                                     DescriptorGenerator.class.getPackage().getName());
-        instructions.addElement("Remote-App").
-                setText("installer;user=\"" + username + "\";date=\""
-                        + System.currentTimeMillis() + "\"" +
-                        ";registration-url=\"" + registrationUrl + "\"");
-
-        Document appDoc = DocumentHelper.createDocument();
-        appDoc.setRootElement(plugin);
-
-        return appDoc;
-    }
-
-    // fixme: this is temporary until UPM supports clear designation of remote apps
-    public static String calculatePluginName(String name)
-    {
-        return name + " (Remote App)";
-    }
-
-    public static class StartedListener
-    {
-        private final String pluginKey;
-        private final CountDownLatch latch;
-
-        private volatile Exception cause;
-
-        public StartedListener(String pluginKey, CountDownLatch latch)
-        {
-            this.pluginKey = pluginKey;
-            this.latch = latch;
-        }
-
-        @EventListener
-        public void onAppStart(RemoteAppStartedEvent event)
-        {
-            if (event.getRemoteAppKey().equals(pluginKey))
-            {
-                latch.countDown();
-            }
-        }
-
-        @EventListener
-        public void onAppStartFailed(RemoteAppStartFailedEvent event)
-        {
-            if (event.getRemoteAppKey().equals(pluginKey))
-            {
-                cause = event.getCause();
-                latch.countDown();
-            }
-        }
-        public Exception getFailedCause()
-        {
-            return cause;
-        }
-    }
-
-    private class ValidateModuleHandler implements ModuleGeneratorManager.ModuleHandler
-    {
-        private final String registrationUrl;
-        private final String username;
-        private final Properties props;
-        private final String pluginKey;
-
-        public ValidateModuleHandler(String registrationUrl, String username,
-                Properties props,
-                String pluginKey)
-        {
-            this.registrationUrl = registrationUrl;
-            this.username = username;
-            this.props = props;
-            this.pluginKey = pluginKey;
-        }
-
-        @Override
-        public void handle(Element element, RemoteModuleGenerator generator)
-        {
-            generator.validate(element, registrationUrl, username);
-            props.putAll(generator.getI18nMessages(pluginKey, element));
-        }
-    }
 }

File plugin/src/main/java/com/atlassian/labs/remoteapps/installer/FileRemoteAppFilter.java

+package com.atlassian.labs.remoteapps.installer;
+
+import com.atlassian.labs.remoteapps.util.BundleUtil;
+import com.atlassian.labs.remoteapps.util.RemoteAppManifestReader;
+import com.atlassian.plugin.util.PluginUtils;
+import org.apache.commons.io.FileUtils;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Serves remote app files when installed from a local file url.  Only allowed in dev mode.
+ */
+public class FileRemoteAppFilter implements Filter
+{
+    private static final Pattern RESOURCE_PATTERN = Pattern.compile("/([-a-zA-Z0-9._]+)/([a-zA-Z0-9-_/]+\\.(?:js|css|html))");
+    private final BundleContext bundleContext;
+
+    public FileRemoteAppFilter(BundleContext bundleContext)
+    {
+        this.bundleContext = bundleContext;
+    }
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException
+    {
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws
+            IOException, ServletException
+    {
+        if (!Boolean.getBoolean(PluginUtils.ATLASSIAN_DEV_MODE))
+        {
+            ((HttpServletResponse)response).sendError(403, "Only allowed in dev mode");
+        }
+
+        HttpServletRequest req = (HttpServletRequest) request;
+        HttpServletResponse res = (HttpServletResponse) response;
+        String path = req.getRequestURI().substring(req.getContextPath().length() + "/app".length());
+        Matcher m = RESOURCE_PATTERN.matcher(path);
+        if (m.matches())
+        {
+            String appKey = m.group(1);
+            String localPath = m.group(2);
+
+            Bundle appBundle = BundleUtil.findBundleForPlugin(bundleContext, appKey);
+            File descriptorFile = new File(
+                    URI.create(RemoteAppManifestReader.getRegistrationUrl(appBundle)));
+            File baseFile = descriptorFile.getParentFile();
+
+            File localFile = new File(baseFile, localPath);
+            if (!localFile.exists())
+            {
+                send404(res);
+                return;
+            }
+            byte[] localData = FileUtils.readFileToByteArray(localFile);
+
+            res.setHeader("Vary", "Accept-Encoding");
+            res.setContentType(findContentType(localFile));
+            res.setContentLength(localData.length);
+
+            res.setHeader("Connection", "keep-alive");
+            ServletOutputStream sos = res.getOutputStream();
+            sos.write(localData);
+            sos.flush();
+            sos.close();
+        }
+        else
+        {
+            send404(res);
+        }
+    }
+
+    private String findContentType(File localFile)
+    {
+        String path = localFile.getAbsolutePath();
+        if (path.endsWith(".js"))
+        {
+            return "application/x-javascript; charset=utf-8";
+        } else if (path.endsWith(".css"))
+        {
+            return "text/css";
+        }
+        else if (path.endsWith(".html"))
+        {
+            return "text/html";
+        }
+        else
+        {
+            throw new IllegalArgumentException("Wrong extension: " + localFile.getAbsolutePath());
+        }
+    }
+
+    private void send404(HttpServletResponse res) throws IOException
+    {
+        res.sendError(HttpServletResponse.SC_NOT_FOUND, "Cannot find resource");
+    }
+
+    @Override
+    public void destroy()
+    {
+    }
+}

File plugin/src/main/java/com/atlassian/labs/remoteapps/installer/FileRemoteAppInstaller.java

+package com.atlassian.labs.remoteapps.installer;
+
+import com.atlassian.event.api.EventPublisher;
+import com.atlassian.labs.remoteapps.DescriptorValidator;
+import com.atlassian.labs.remoteapps.ModuleGeneratorManager;
+import com.atlassian.labs.remoteapps.OAuthLinkManager;
+import com.atlassian.labs.remoteapps.api.DescriptorGenerator;
+import com.atlassian.labs.remoteapps.api.InstallationFailedException;
+import com.atlassian.labs.remoteapps.api.PermissionDeniedException;
+import com.atlassian.labs.remoteapps.event.RemoteAppInstalledEvent;
+import com.atlassian.labs.remoteapps.modules.external.RemoteModuleGenerator;
+import com.atlassian.labs.remoteapps.modules.page.jira.JiraProfileTabModuleGenerator;
+import com.atlassian.labs.remoteapps.util.zip.ZipBuilder;
+import com.atlassian.labs.remoteapps.util.zip.ZipHandler;
+import com.atlassian.plugin.*;
+import com.atlassian.plugin.util.PluginUtils;
+import com.atlassian.sal.api.ApplicationProperties;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.dom4j.Document;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.net.URI;
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static com.atlassian.labs.remoteapps.util.Dom4jUtils.getOptionalUriAttribute;
+import static com.atlassian.labs.remoteapps.util.Dom4jUtils.getRequiredAttribute;
+
+/**
+ * Installs remote apps from a local file descriptor.  Only allowed in dev mode.
+ */
+@Component
+public class FileRemoteAppInstaller implements RemoteAppInstaller
+{
+    private final PluginController pluginController;
+    private final DescriptorValidator descriptorValidator;
+    private final PluginAccessor pluginAccessor;
+    private final OAuthLinkManager oAuthLinkManager;
+    private final FormatConverter formatConverter;
+    private final ApplicationProperties applicationProperties;
+    private final InstallerHelper installerHelper;
+
+    @Autowired
+    public FileRemoteAppInstaller(PluginController pluginController,
+            DescriptorValidator descriptorValidator, PluginAccessor pluginAccessor,
+            OAuthLinkManager oAuthLinkManager, FormatConverter formatConverter,
+            ApplicationProperties applicationProperties, InstallerHelper installerHelper)
+    {
+        this.pluginController = pluginController;
+        this.installerHelper = installerHelper;
+        this.descriptorValidator = descriptorValidator;
+        this.pluginAccessor = pluginAccessor;
+        this.oAuthLinkManager = oAuthLinkManager;
+        this.formatConverter = formatConverter;
+        this.applicationProperties = applicationProperties;
+    }
+
+    @Override
+    public String install(String username, URI registrationUrl, String registrationSecret,
+            boolean stripUnknownModules, KeyValidator keyValidator) throws
+            PermissionDeniedException
+    {
+        if (!Boolean.getBoolean(PluginUtils.ATLASSIAN_DEV_MODE))
+        {
+            throw new PermissionDeniedException("File descriptors are only accepted in dev mode");
+        }
+
+        String contentType = registrationUrl.getPath().endsWith(".json") ? "application/json" :
+                             registrationUrl.getPath().endsWith(".yaml") ? "text/yaml" :
+                                                                           "text/xml";
+
+        File descriptorFile = new File(registrationUrl);
+        if (!descriptorFile.exists())
+        {
+            throw new PermissionDeniedException("Descriptor out found: " + registrationUrl);
+        }
+
+        String descriptorText = null;
+        try
+        {
+            descriptorText = FileUtils.readFileToString(descriptorFile);
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException("Unable to read descriptor", e);
+        }
+
+        // this is still much too similar to DefaultRemoteAppInstaller, but leaving it as
+        // the other class has the nice docco comments
+        Document document = formatConverter.toDocument(
+                registrationUrl.toString(), contentType, descriptorText);
+        final Element root = document.getRootElement();
+        final String pluginKey = root.attributeValue("key");
+        URI displayUrl = URI.create(applicationProperties.getBaseUrl() + "/app/" + pluginKey);            // fix me
+        root.addAttribute("display-url", displayUrl.toString());
+
+        descriptorValidator.validate(registrationUrl, document);
+
+
+        keyValidator.validatePermissions(pluginKey);
+        Plugin plugin = pluginAccessor.getPlugin(pluginKey);
+
+        if (plugin != null)
+        {
+            pluginController.uninstall(plugin);
+        }
+        else
+        {
+            if (oAuthLinkManager.isAppAssociated(pluginKey))
+            {
+                throw new PermissionDeniedException("App key '" + pluginKey
+                        + "' is already associated with an OAuth link");
+            }
+        }
+
+
+        final Properties i18nMessages = installerHelper.validateAndGenerateMessages(root, pluginKey, displayUrl, username);
+
+        Document pluginXml = installerHelper.generatePluginDescriptor(username,
+                registrationUrl, document);
+
+
+        JarPluginArtifact jar = installerHelper.createJarPluginArtifact(pluginKey,
+                registrationUrl.getHost(), pluginXml, document, i18nMessages);
+
+        installerHelper.installRemoteAppPlugin(username, pluginKey, jar);
+
+        return pluginKey;
+    }
+}

File plugin/src/main/java/com/atlassian/labs/remoteapps/installer/FormatConverter.java

 import org.yaml.snakeyaml.constructor.SafeConstructor;
 
 import java.io.StringReader;
-import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import static com.atlassian.labs.remoteapps.util.Dom4jUtils.printDocument;
+import static com.atlassian.labs.remoteapps.util.Dom4jUtils.printNode;
 
 @Component
 public class FormatConverter
         }
         if (log.isDebugEnabled())
         {
-            log.debug("Transformed YAML to\n" + printDocument(doc));
+            log.debug("Transformed YAML to\n" + printNode(doc));
         }
         return doc;
     }

File plugin/src/main/java/com/atlassian/labs/remoteapps/installer/InstallerHelper.java

+package com.atlassian.labs.remoteapps.installer;
+
+import com.atlassian.event.api.EventPublisher;
+import com.atlassian.labs.remoteapps.ModuleGeneratorManager;
+import com.atlassian.labs.remoteapps.api.DescriptorGenerator;
+import com.atlassian.labs.remoteapps.api.InstallationFailedException;
+import com.atlassian.labs.remoteapps.event.RemoteAppInstalledEvent;
+import com.atlassian.labs.remoteapps.modules.external.RemoteModuleGenerator;
+import com.atlassian.labs.remoteapps.modules.page.jira.JiraProfileTabModuleGenerator;
+import com.atlassian.labs.remoteapps.util.zip.ZipBuilder;
+import com.atlassian.labs.remoteapps.util.zip.ZipHandler;
+import com.atlassian.plugin.JarPluginArtifact;
+import com.atlassian.plugin.PluginController;
+import com.atlassian.plugin.PluginParseException;
+import org.dom4j.Document;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.net.URI;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static com.atlassian.labs.remoteapps.util.Dom4jUtils.getRequiredAttribute;
+
+@Component
+public class InstallerHelper
+{
+    private static final int INSTALLATION_TIMEOUT = 10;
+    private final EventPublisher eventPublisher;
+    private final PluginController pluginController;
+    private final ModuleGeneratorManager moduleGeneratorManager;
+
+    private static final Logger log = LoggerFactory.getLogger(InstallerHelper.class);
+
+    @Autowired
+    public InstallerHelper(EventPublisher eventPublisher, PluginController pluginController,
+            ModuleGeneratorManager moduleGeneratorManager)
+    {
+        this.eventPublisher = eventPublisher;
+        this.pluginController = pluginController;
+        this.moduleGeneratorManager = moduleGeneratorManager;
+    }
+
+    public void installRemoteAppPlugin(String username, String pluginKey, JarPluginArtifact jar)
+    {
+        final CountDownLatch latch = new CountDownLatch(1);
+        StartedListener startListener = new StartedListener(pluginKey, latch);
+        eventPublisher.register(startListener);
+
+
+        try
+        {
+            pluginController.installPlugins(jar);
+
+
+            if (!latch.await(INSTALLATION_TIMEOUT, TimeUnit.SECONDS))
+            {
+                Exception cause = startListener.getFailedCause();
+                if (cause != null)
+                {
+                    log.info("Remote app '{}' was not started successfully and is "
+                            + "disabled due to: {}", pluginKey,
+                            cause);
+                    throw new InstallationFailedException("Error starting app: "
+                            + cause.getMessage(),
+                            cause);
+                }
+                else
+                {
+                    log.info("Remote app '{}' was not started successfully in "
+                            + "the expected {} seconds.", pluginKey, INSTALLATION_TIMEOUT);
+                    throw new InstallationFailedException("Timeout starting app");
+                }
+            }
+        }
+        catch (InterruptedException e)
+        {
+            // ignore
+        }
+        finally
+        {
+            eventPublisher.unregister(startListener);
+        }
+
+        log.info("Registered app '{}' by '{}'", pluginKey, username);
+
+        eventPublisher.publish(new RemoteAppInstalledEvent(pluginKey));
+    }
+
+    public JarPluginArtifact createJarPluginArtifact(final String pluginKey,
+            String host, final Document pluginXml, final Document appXml, final Properties props)
+    {
+        return new JarPluginArtifact(
+                ZipBuilder.buildZip("install-" + host, new ZipHandler()
+                {
+                    @Override
+                    public void build(ZipBuilder builder) throws IOException
+                    {
+                        attachResources(pluginKey, props, pluginXml, builder);
+                        builder.addFile("atlassian-plugin.xml", pluginXml);
+                        builder.addFile("META-INF/spring/remoteapps-loader.xml", getClass().getResourceAsStream("remoteapps-loader.xml"));
+                        builder.addFile("atlassian-remote-app.xml", appXml);
+                    }
+                }));
+    }
+
+    // fixme: this is temporary until UPM supports clear designation of remote apps
+    public static String calculatePluginName(String name)
+    {
+        return name + " (Remote App)";
+    }
+
+    private void attachResources(String pluginKey, Properties props,
+            Document pluginXml, ZipBuilder builder
+    ) throws IOException
+    {
+        final StringWriter writer = new StringWriter();
+        try
+        {
+            props.store(writer, "");
+        }
+        catch (IOException e)
+        {
+            // shouldn't happen
+            throw new RuntimeException(e);
+        }
+
+        pluginXml.getRootElement().addElement("resource")
+                .addAttribute("type", "i18n")
+                .addAttribute("name", "i18n")
+                .addAttribute("location", pluginKey.hashCode() + ".i18n");
+
+        builder.addFile(pluginKey.hashCode() + "/i18n.properties",
+                writer.toString());
+    }
+
+    public Document generatePluginDescriptor(String username,
+            URI registrationUrl, Document doc)
+    {
+        Element oldRoot = doc.getRootElement();
+
+        final Element plugin = DocumentHelper.createElement("atlassian-plugin");
+        plugin.addAttribute("plugins-version", "2");
+        plugin.addAttribute("key", getRequiredAttribute(oldRoot, "key"));
+        plugin.addAttribute("name", calculatePluginName(getRequiredAttribute(oldRoot, "name")));
+        Element info = plugin.addElement("plugin-info");
+        info.addElement("version").setText(
+                getRequiredAttribute(oldRoot, "version"));
+
+        moduleGeneratorManager.processDescriptor(oldRoot,
+                new ModuleGeneratorManager.ModuleHandler()
+                {
+                    @Override
+                    public void handle(
+                            Element element,
+                            RemoteModuleGenerator generator)
+                    {
+                        generator.generatePluginDescriptor(
+                                element,
+                                plugin);
+                    }
+                });
+
+        if (oldRoot.element("vendor") != null)
+        {
+            info.add(oldRoot.element("vendor").detach());
+        }
+        Element instructions = info.addElement("bundle-instructions");
+        instructions
+                .addElement("Import-Package")
+                .setText(
+                        JiraProfileTabModuleGenerator.class.getPackage().getName() +
+                                ";resolution:=optional," +
+                                "com.atlassian.jira.plugin.searchrequestview;resolution:=optional," +                                     DescriptorGenerator.class.getPackage().getName());
+        instructions.addElement("Remote-App").
+                setText("installer;user=\"" + username + "\";date=\""
+                        + System.currentTimeMillis() + "\"" +
+                        ";registration-url=\"" + registrationUrl + "\"");
+
+        Document appDoc = DocumentHelper.createDocument();
+        appDoc.setRootElement(plugin);
+
+        return appDoc;
+    }
+
+    public void detachUnknownModuleElements(Document document)
+    {
+        Set<String> validModuleTypes = moduleGeneratorManager
+                .getModuleGeneratorKeys();
+        for (Element child : (List<Element>)document.getRootElement().elements())
+        {
+            if (!validModuleTypes.contains(child.getName()))
+            {
+                log.debug("Stripping unknown module '{}'", child.getName());
+                child.detach();
+            }
+        }
+    }
+
+    public Properties validateAndGenerateMessages(Element root, String pluginKey,
+            URI registrationUrl, String username)
+    {
+        final Properties i18nMessages = new Properties();
+        try
+        {
+            moduleGeneratorManager.getApplicationTypeModuleGenerator()
+                    .validate(root, registrationUrl, username);
+
+            ValidateModuleHandler moduleValidator = new ValidateModuleHandler(
+                    registrationUrl,
+                    username,
+                    i18nMessages,
+                    pluginKey);
+            moduleGeneratorManager.processDescriptor(root, moduleValidator);
+        }
+        catch (PluginParseException ex)
+        {
+            throw new InstallationFailedException(
+                    "Validation of the descriptor failed: " + ex.getMessage(),
+                    ex);
+        }
+        return i18nMessages;
+    }
+
+}

File plugin/src/main/java/com/atlassian/labs/remoteapps/installer/RemoteAppInstaller.java

 
 import com.atlassian.labs.remoteapps.api.PermissionDeniedException;
 
+import java.net.URI;
+
 /**
  * Installs a remote app
  */
      * @param registrationSecret The secret token to send to the registration URL.  Can be null.
      * @param stripUnknownModules Whether unknown modules should be stripped
      */
-    String install(String username, String registrationUrl, String registrationSecret,
+    String install(String username, URI registrationUrl, String registrationSecret,
             boolean stripUnknownModules, KeyValidator keyValidator) throws
                                                                                                                   PermissionDeniedException;
 }

File plugin/src/main/java/com/atlassian/labs/remoteapps/installer/SchemeDelegatingRemoteAppInstaller.java

+package com.atlassian.labs.remoteapps.installer;
+
+import com.atlassian.labs.remoteapps.api.PermissionDeniedException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.net.URI;
+
+/**
+ * Determines the correct installer impl from the uri scheme
+ */
+@Component
+public class SchemeDelegatingRemoteAppInstaller implements RemoteAppInstaller
+{
+    private final DefaultRemoteAppInstaller httpInstaller;
+    private final FileRemoteAppInstaller fileInstaller;
+
+    @Autowired
+    public SchemeDelegatingRemoteAppInstaller(DefaultRemoteAppInstaller httpInstaller,
+            FileRemoteAppInstaller fileInstaller)
+    {
+        this.httpInstaller = httpInstaller;
+        this.fileInstaller = fileInstaller;
+    }
+
+    @Override
+    public String install(String username, URI registrationUrl, String registrationSecret,
+            boolean stripUnknownModules, KeyValidator keyValidator) throws
+            PermissionDeniedException
+    {
+        if ("file".equals(registrationUrl.getScheme()))
+        {
+            return fileInstaller.install(username, registrationUrl, registrationSecret,
+                    stripUnknownModules, keyValidator);
+        }
+        else
+        {
+            return httpInstaller.install(username, registrationUrl, registrationSecret,
+                    stripUnknownModules, keyValidator);
+        }
+    }
+}

File plugin/src/main/java/com/atlassian/labs/remoteapps/installer/StartedListener.java

+package com.atlassian.labs.remoteapps.installer;
+
+import com.atlassian.event.api.EventListener;
+import com.atlassian.labs.remoteapps.event.RemoteAppStartFailedEvent;
+import com.atlassian.labs.remoteapps.event.RemoteAppStartedEvent;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+* Listens for the remote app to start
+*/
+public class StartedListener
+{
+    private final String pluginKey;
+    private final CountDownLatch latch;
+
+    private volatile Exception cause;
+
+    public StartedListener(String pluginKey, CountDownLatch latch)
+    {
+        this.pluginKey = pluginKey;
+        this.latch = latch;
+    }
+
+    @EventListener
+    public void onAppStart(RemoteAppStartedEvent event)
+    {
+        if (event.getRemoteAppKey().equals(pluginKey))
+        {
+            latch.countDown();
+        }
+    }
+
+    @EventListener
+    public void onAppStartFailed(RemoteAppStartFailedEvent event)
+    {
+        if (event.getRemoteAppKey().equals(pluginKey))
+        {
+            cause = event.getCause();
+            latch.countDown();
+        }
+    }
+    public Exception getFailedCause()
+    {
+        return cause;
+    }
+}

File plugin/src/main/java/com/atlassian/labs/remoteapps/installer/ValidateModuleHandler.java

+package com.atlassian.labs.remoteapps.installer;
+
+import com.atlassian.labs.remoteapps.ModuleGeneratorManager;
+import com.atlassian.labs.remoteapps.modules.external.RemoteModuleGenerator;
+import org.dom4j.Element;
+
+import java.net.URI;
+import java.util.Properties;
+
+/**
+* Validates modules and stores i18n file
+*/
+class ValidateModuleHandler implements ModuleGeneratorManager.ModuleHandler
+{
+    private final URI registrationUrl;
+    private final String username;
+    private final Properties props;
+    private final String pluginKey;
+
+    public ValidateModuleHandler(URI registrationUrl, String username,
+            Properties props,
+            String pluginKey)
+    {
+        this.registrationUrl = registrationUrl;
+        this.username = username;
+        this.props = props;
+        this.pluginKey = pluginKey;
+    }
+
+    @Override
+    public void handle(Element element, RemoteModuleGenerator generator)
+    {
+        generator.validate(element, registrationUrl, username);
+        props.putAll(generator.getI18nMessages(pluginKey, element));
+    }
+}

File plugin/src/main/java/com/atlassian/labs/remoteapps/loader/RemoteAppLoader.java

 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
+import java.net.URI;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
             bundle will validated.  This validation involves processing the descriptor against the
             generated XML Schema for this Atlassian application instance.
              */
-            descriptorValidator.validate("atlassian-remote-app.xml", appDescriptor);
+            descriptorValidator.validate(URI.create("atlassian-remote-app.xml"), appDescriptor);
 
             /*!
             ### Step 2 - Remote Module Registration

File plugin/src/main/java/com/atlassian/labs/remoteapps/modules/ApplicationLinkOperationsFactory.java

 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Future;
 
 import static com.atlassian.labs.remoteapps.util.ServletUtils.encodeGetUrl;
 import static com.google.common.collect.Maps.newHashMap;
     public static interface LinkOperations
     {
         ApplicationLink get();
-        String signGetUrl(String user