changeset 768:7d2759e4bc98

Backported enabled access to manifests' attributes from JNLPFile class, implemented http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/manifest.html#app_name
author Jiri Vanek <jvanek@redhat.com>
date Wed, 13 Nov 2013 11:15:18 +0100
parents acbada276d23
children e139942101a9
files ChangeLog NEWS netx/net/sourceforge/jnlp/InformationDesc.java netx/net/sourceforge/jnlp/JNLPFile.java netx/net/sourceforge/jnlp/PluginBridge.java netx/net/sourceforge/jnlp/ResourcesDesc.java netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java netx/net/sourceforge/jnlp/security/CertWarningPane.java netx/net/sourceforge/jnlp/util/StreamUtils.java tests/netx/unit/net/sourceforge/jnlp/runtime/JNLPFileTest.java tests/netx/unit/net/sourceforge/jnlp/runtime/ResourcesDescTest.java tests/test-extensions/net/sourceforge/jnlp/mock/DummyJNLPFileWithJar.java tests/test-extensions/net/sourceforge/jnlp/util/FileTestUtils.java
diffstat 13 files changed, 741 insertions(+), 44 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Wed Nov 06 14:46:43 2013 +0100
+++ b/ChangeLog	Wed Nov 13 11:15:18 2013 +0100
@@ -1,3 +1,27 @@
+2013-11-13  Jiri Vanek  <jvanek@redhat.com>
+
+	Enabled access to manifests' attributes from JNLPFile class
+	Implemented http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/manifest.html#app_name
+	* netx/net/sourceforge/jnlp/JNLPFile.java: Added (manifestsAttributes) field.
+	Added (ManifestsAttributes) inner class, to encapsulate access to attributes.
+	(getTitle) can handle manifests too.
+	* netx/net/sourceforge/jnlp/PluginBridge.java: is following app_name recommendations.
+	* netx/net/sourceforge/jnlp/ResourcesDesc.java: (getMainJAR) made more granular
+	* netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java: (init) inject itself
+	to file's ManifestsAttributes. (checkForAttributeInJars) renamed field
+	mainClassInThisJar to attributeInThisJar. Added getter for mainClass.
+	* netx/net/sourceforge/jnlp/security/CertWarningPane.java: bracketing cleanup.
+	* tests/netx/unit/net/sourceforge/jnlp/runtime/JNLPFileTest.java: new test to
+	check new functionalites
+	* tests/netx/unit/net/sourceforge/jnlp/runtime/ResourcesDescTest.java: same 
+	* tests/test-extensions/net/sourceforge/jnlp/mock/DummyJNLPFileWithJar.java:
+	can set info
+	* NEWS: mentioned first u45 attribute, dialog centering
+	* tests/test-extensions/net/sourceforge/jnlp/mock/DummyJNLPFileWithJar.java: backported
+	* tests/test-extensions/net/sourceforge/jnlp/util/FileTestUtils.java: backported
+	* netx/net/sourceforge/jnlp/InformationDesc.java: partial backport of constructor change
+
+
 2013-11-06  Jiri Vanek  <jvanek@redhat.com>
 
 	Enabled java console for plugin
--- a/NEWS	Wed Nov 06 14:46:43 2013 +0100
+++ b/NEWS	Wed Nov 13 11:15:18 2013 +0100
@@ -10,6 +10,7 @@
 
 New in release 1.4.2 (2013-MM-DD):
 * Dialogs center on screen before becoming visible
+* Support for u45 new manifest attributes (Application-Name)
 * Plugin
   - RH976833: Multiple applets on one page cause deadlock
   - Enabled javaconsole
--- a/netx/net/sourceforge/jnlp/InformationDesc.java	Wed Nov 06 14:46:43 2013 +0100
+++ b/netx/net/sourceforge/jnlp/InformationDesc.java	Wed Nov 13 11:15:18 2013 +0100
@@ -52,18 +52,21 @@
     /** the data as list of key,value pairs */
     private List<Object> info;
 
-    /** the JNLPFile this information is for */
-    private JNLPFile jnlpFile;
 
     /**
      * Create an information element object.
      *
-     * @param jnlpFile file that the information is for
      * @param locales the locales the information is for
      */
+    public InformationDesc(Locale locales[]) {
+        this.locales = locales;
+    }
+
+   /**
+    * 1.4 comaptibility
+    */
     public InformationDesc(JNLPFile jnlpFile, Locale locales[]) {
-        this.jnlpFile = jnlpFile;
-        this.locales = locales;
+        this(locales);
     }
 
     /**
@@ -171,6 +174,8 @@
             }
         }
 
+        // FIXME if there's no larger icon, choose the closest smaller icon
+        // instead of the first
         if (best == null)
             best = icons[0];
 
@@ -185,13 +190,6 @@
     }
 
     /**
-     * Returns the JNLPFile the information is for.
-     */
-    public JNLPFile getJNLPFile() {
-        return jnlpFile;
-    }
-
-    /**
      * Returns whether offline execution allowed.
      */
     public boolean isOfflineAllowed() {
--- a/netx/net/sourceforge/jnlp/JNLPFile.java	Wed Nov 06 14:46:43 2013 +0100
+++ b/netx/net/sourceforge/jnlp/JNLPFile.java	Wed Nov 13 11:15:18 2013 +0100
@@ -28,9 +28,11 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
+import java.util.jar.Attributes;
 
 import net.sourceforge.jnlp.cache.ResourceTracker;
 import net.sourceforge.jnlp.cache.UpdatePolicy;
+import net.sourceforge.jnlp.runtime.JNLPClassLoader;
 import net.sourceforge.jnlp.runtime.JNLPRuntime;
 
 /**
@@ -51,6 +53,13 @@
  * @version $Revision: 1.21 $
  */
 public class JNLPFile {
+    
+    
+    public static final String APP_NAME = "Application-Name";
+    public static final String CALLER_ALLOWABLE = "Caller-Allowable-Codebase";
+    public static final String APP_LIBRARY_ALLOWABLE = "Application-Library-Allowable-Codebase";
+        
+    
 
     // todo: save the update policy, then if file was not updated
     // then do not check resources for being updated.
@@ -118,6 +127,10 @@
      * List of acceptable properties (not-special)
      */
     private String[] generalProperties = SecurityDesc.getJnlpRIAPermissions();
+    
+    /** important manifests' attributes */
+    private final ManifestsAttributes manifestsAttributes = new ManifestsAttributes();
+
 
     { // initialize defaults if security allows
         try {
@@ -301,10 +314,46 @@
     /**
      * Returns the JNLP file's best localized title. This method returns the same
      * value as InformationDesc.getTitle().
+     * 
+     * Since jdk7 u45, also manifest title, and mainclass are taken to consideration;
+     * See PluginBridge
      */
     public String getTitle() {
+        String jnlpTitle = getTitleFromJnlp();
+        String manifestTitle = getTitleFromManifest();
+        if (jnlpTitle != null && manifestTitle != null) {
+            if (jnlpTitle.equals(manifestTitle)) {
+                return jnlpTitle;
+            }
+            return jnlpTitle+" ("+manifestTitle+")";
+        }
+        if (jnlpTitle != null && manifestTitle == null) {
+            return jnlpTitle;
+        }
+        if (jnlpTitle == null && manifestTitle != null) {
+            return manifestTitle;
+        }
+        String mainClass = getManifestsAttributes().getMainClass();
+        return mainClass;        
+    }
+    
+    /**
+     * Returns the JNLP file's best localized title. This method returns the same
+     * value as InformationDesc.getTitle().
+     */
+    public String getTitleFromJnlp() {
         return getInformation().getTitle();
     }
+    
+    public String getTitleFromManifest() {
+        String inManifestTitle = getManifestsAttributes().getApplicationName();
+        if (inManifestTitle == null && getManifestsAttributes().isLoader()){
+            System.err.println("Application title was not found in manifest. Check with application vendor");
+        }
+        return inManifestTitle;
+    }
+    
+    
 
     /**
      * Returns the JNLP file's best localized vendor. This method returns the same
@@ -820,4 +869,80 @@
     public void setSignedJNLPAsMissing() {
         missingSignedJNLP = true;
     }
+
+    public ManifestsAttributes getManifestsAttributes() {
+        return manifestsAttributes;
+    }
+    
+    
+ public class ManifestsAttributes{
+        private JNLPClassLoader loader;
+        
+        
+        public void setLoader(JNLPClassLoader loader) {
+            this.loader = loader;
+        }
+
+        public boolean isLoader() {
+            return loader != null;
+        }
+        
+        
+
+        /**
+         * main class can be defined outside of manifest.
+         * This method is mostly for completeness
+         */
+        public String getMainClass(){
+            if (loader == null) {
+                 if (JNLPRuntime.isDebug()) {
+                     System.err.println("Jars not ready to provide main class");
+                }
+                return null;    
+            }
+            return loader.getMainClass();
+        }
+        
+        /**
+         * http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/manifest.html#app_name
+         */
+        public String getApplicationName(){
+            return getAttribute(APP_NAME);
+        }
+        
+        /**
+         * http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/manifest.html#caller_allowable
+         */
+         public String getCallerAllowableCodebase(){
+            return getAttribute(CALLER_ALLOWABLE);
+        }
+
+        /**
+         * http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/manifest.html#app_library
+         */
+         public String getApplicationLibraryAllowableCodebase(){
+            return getAttribute(APP_LIBRARY_ALLOWABLE);
+        }
+         
+        /**
+         * get custom attribute.
+         */
+        public String getAttribute(String name){
+            return getAttribute(new Attributes.Name(name));
+        }
+        
+        /**
+         * get standard attribute
+         */
+        public String getAttribute(Attributes.Name name){
+          if (loader == null) {
+                 if (JNLPRuntime.isDebug()) {
+                     System.err.println("Jars not ready to provide attribute "+ name);
+                }
+                return null;    
+            }
+            return loader.checkForAttributeInJars(Arrays.asList(getResources().getJARs()), name);
+        }
+    }
+ 
 }
--- a/netx/net/sourceforge/jnlp/PluginBridge.java	Wed Nov 06 14:46:43 2013 +0100
+++ b/netx/net/sourceforge/jnlp/PluginBridge.java	Wed Nov 13 11:15:18 2013 +0100
@@ -179,7 +179,7 @@
 
         // the class name should be of the form foo.bar.Baz not foo/bar/Baz
         String mainClass = main.replace('/', '.');
-        launchType = new AppletDesc(params.getAppletTitle(), mainClass, documentBase, width,
+        launchType = new AppletDesc(getTitle(), mainClass, documentBase, width,
                                     height, params.getUnmodifiableMap());
 
         if (main.endsWith(".class")) //single class file only
@@ -226,7 +226,18 @@
         return new DownloadOptions(usePack, useVersion);
     }
 
+    @Override
     public String getTitle() {
+        String inManifestTitle = super.getTitleFromManifest();
+        if (inManifestTitle != null) {
+            return inManifestTitle;
+        }
+        //specification is recommending  main class instead of html parameter
+        //http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/manifest.html#app_name
+        String mainClass = getManifestsAttributes().getMainClass();
+        if (mainClass != null) {
+            return mainClass;
+        }
         return params.getAppletTitle();
     }
 
--- a/netx/net/sourceforge/jnlp/ResourcesDesc.java	Wed Nov 06 14:46:43 2013 +0100
+++ b/netx/net/sourceforge/jnlp/ResourcesDesc.java	Wed Nov 13 11:15:18 2013 +0100
@@ -67,23 +67,30 @@
         return resources.toArray(new JREDesc[resources.size()]);
     }
 
+    public static JARDesc getMainJAR(JARDesc jars[] ) {
+        return getMainJAR(Arrays.asList(jars));
+    }
+
+    public static JARDesc getMainJAR(List<JARDesc> jars) {
+        for (JARDesc jar : jars) {
+            if (jar.isMain()) {
+                return jar;
+            }
+        }
+
+        if (jars.size() > 0) {
+            return jars.get(0);
+        } else {
+            return null;
+        }
+    }
     /**
      * Returns the main JAR for these resources.  There first JAR
      * is returned if no JARs are specified as the main JAR, and if
      * there are no JARs defined then null is returned.
      */
     public JARDesc getMainJAR() {
-        JARDesc jars[] = getJARs();
-
-        for (JARDesc jar : jars) {
-            if (jar.isMain())
-                return jar;
-        }
-
-        if (jars.length > 0)
-            return jars[0];
-        else
-            return null;
+        return getMainJAR(getJARs());
     }
 
     /**
--- a/netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java	Wed Nov 06 14:46:43 2013 +0100
+++ b/netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java	Wed Nov 13 11:15:18 2013 +0100
@@ -112,7 +112,7 @@
     /** Signed JNLP File and Template */
     final public static String TEMPLATE = "JNLP-INF/APPLICATION_TEMPLATE.JNLP";
     final public static String APPLICATION = "JNLP-INF/APPLICATION.JNLP";
-    
+
     /** Actions to specify how cache is to be managed **/
     public static enum DownloadAction {
         DOWNLOAD_TO_CACHE, REMOVE_FROM_CACHE, CHECK_CACHE
@@ -200,7 +200,7 @@
 
     /** Name of the application's main class */
     private String mainClass = null;
-
+    
     /**
      * Variable to track how many times this loader is in use
      */
@@ -244,6 +244,9 @@
 
         this.mainClass = mainName;
 
+        //as it is harmless, we can set is as soon as possible.
+        file.getManifestsAttributes().setLoader(this);
+        
         AppVerifier verifier;
 
         if (file instanceof PluginBridge && !((PluginBridge)file).useJNLPHref()) {
@@ -258,13 +261,15 @@
         initializeExtensions();
 
         initializeResources();
+        
 
         // initialize permissions
         initializePermissions();
 
         setSecurity();
-
+        
         installShutdownHooks();
+        
 
     }
 
@@ -807,7 +812,7 @@
         String result = null;
         
         // Check main jar
-        JARDesc mainJarDesc = file.getResources().getMainJAR();
+        JARDesc mainJarDesc = ResourcesDesc.getMainJAR(jars);
         result = getManifestAttribute(mainJarDesc.getLocation(), name);
 
         if (result != null) {
@@ -824,10 +829,10 @@
 
         // Still not found? Iterate and set if only 1 was found
         for (JARDesc jarDesc: jars) {
-            String mainClassInThisJar = getManifestAttribute(jarDesc.getLocation(), name);
-                if (mainClassInThisJar != null) {
+            String attributeInThisJar = getManifestAttribute(jarDesc.getLocation(), name);
+                if (attributeInThisJar != null) {
                     if (result == null) { // first main class
-                        result = mainClassInThisJar;
+                        result = attributeInThisJar;
                     } else { // There is more than one main class. Set to null and break.
                         result = null;
                         break;
@@ -2349,6 +2354,10 @@
 
         return new AccessControlContext(new ProtectionDomain[] { pd });
     }
+    
+    public String getMainClass() {
+        return mainClass;
+    }
 
     /*
      * Helper class to expose protected URLClassLoader methods.
@@ -2473,6 +2482,6 @@
             return null;
         }
     }
-
-
+    
+    
 }
--- a/netx/net/sourceforge/jnlp/security/CertWarningPane.java	Wed Nov 06 14:46:43 2013 +0100
+++ b/netx/net/sourceforge/jnlp/security/CertWarningPane.java	Wed Nov 13 11:15:18 2013 +0100
@@ -106,13 +106,14 @@
         //these strings -- we just want to fill in as many as possible.
         try {
             if ((certVerifier instanceof HttpsCertVerifier) &&
-                             (c instanceof X509Certificate))
+                             (c instanceof X509Certificate)) {
                 name = SecurityUtil.getCN(((X509Certificate) c)
                                         .getSubjectX500Principal().getName());
-            else if (file instanceof PluginBridge)
+            } else if (file instanceof PluginBridge) {
                 name = file.getTitle();
-            else
+            } else {
                 name = file.getInformation().getTitle();
+            }
         } catch (Exception e) {
         }
 
@@ -125,10 +126,11 @@
         }
 
         try {
-            if (file instanceof PluginBridge)
+            if (file instanceof PluginBridge) {
                 from = file.getCodeBase().getHost();
-            else
+            } else {
                 from = file.getInformation().getHomepage().toString();
+            }
         } catch (Exception e) {
         }
 
@@ -145,7 +147,7 @@
             topLabelText = R("SHttpsUnverified") + " " + R("Continue");
             propertyName = "OptionPane.warningIcon";
             iconLocation += "warning.png";
-        } else
+        } else {
             switch (type) {
                 case VERIFIED:
                     topLabelText = R("SSigVerified");
@@ -166,7 +168,7 @@
                     bottomLabelText += " " + R("SWarnFullPermissionsIgnorePolicy");
                     break;
             }
-
+        }
         ImageIcon icon = new ImageIcon((new sun.misc.Launcher())
                                 .getClassLoader().getResource(iconLocation));
         JLabel topLabel = new JLabel(htmlWrap(topLabelText), icon, SwingConstants.LEFT);
@@ -194,8 +196,9 @@
         infoPanel.add(nameLabel);
         infoPanel.add(publisherLabel);
 
-        if (!(certVerifier instanceof HttpsCertVerifier))
+        if (!(certVerifier instanceof HttpsCertVerifier)) {
             infoPanel.add(fromLabel);
+        }
 
         infoPanel.add(alwaysTrust);
         infoPanel.setBorder(BorderFactory.createEmptyBorder(25, 25, 25, 25));
@@ -224,7 +227,7 @@
         add(infoPanel);
         add(buttonPanel);
 
-        JLabel bottomLabel = new JLabel(htmlWrap(bottomLabelText));;
+        JLabel bottomLabel = new JLabel(htmlWrap(bottomLabelText));
         JButton moreInfo = new JButton(R("ButMoreInformation"));
         moreInfo.addActionListener(new MoreInfoButtonListener());
 
--- a/netx/net/sourceforge/jnlp/util/StreamUtils.java	Wed Nov 06 14:46:43 2013 +0100
+++ b/netx/net/sourceforge/jnlp/util/StreamUtils.java	Wed Nov 13 11:15:18 2013 +0100
@@ -42,6 +42,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
 
 public class StreamUtils {
 
@@ -51,7 +52,7 @@
      * 
      * @param stream the stream that will be closed
      */
-    public static void closeSilently (Closeable stream) {
+    public static void closeSilently(Closeable stream) {
         if (stream != null) {
             try {
                 stream.close();
@@ -61,8 +62,27 @@
         }
     }
 
+    /**
+     * Copy an input stream's contents into an output stream.
+     */
+    public static void copyStream(InputStream input, OutputStream output)
+            throws IOException {
+        byte[] buffer = new byte[1024];
+        while (true) {
+            int bytesRead = input.read(buffer);
+            if (bytesRead == -1) {
+                break;
+            }
+            output.write(buffer, 0, bytesRead);
+        }
+    }
 
-    public static String readStreamAsString(InputStream stream) throws IOException {
+    public static String readStreamAsString(InputStream stream)  throws IOException {
+        return readStreamAsString(stream, false);
+    }
+    
+    public static String readStreamAsString(InputStream stream, boolean includeEndOfLines)
+            throws IOException {
         InputStreamReader is = new InputStreamReader(stream);
         StringBuilder sb = new StringBuilder();
         BufferedReader br = new BufferedReader(is);
@@ -72,6 +92,9 @@
                 break;
             }
             sb.append(read);
+            if (includeEndOfLines){
+                sb.append('\n');
+            }
 
         }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/netx/unit/net/sourceforge/jnlp/runtime/JNLPFileTest.java	Wed Nov 13 11:15:18 2013 +0100
@@ -0,0 +1,162 @@
+/* 
+ Copyright (C) 2013 Red Hat, Inc.
+
+ This file is part of IcedTea.
+
+ IcedTea is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, version 2.
+
+ IcedTea is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with IcedTea; see the file COPYING.  If not, write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ 02110-1301 USA.
+
+ Linking this library statically or dynamically with other modules is
+ making a combined work based on this library.  Thus, the terms and
+ conditions of the GNU General Public License cover the whole
+ combination.
+
+ As a special exception, the copyright holders of this library give you
+ permission to link this library with independent modules to produce an
+ executable, regardless of the license terms of these independent
+ modules, and to copy and distribute the resulting executable under
+ terms of your choice, provided that you also meet, for each linked
+ independent module, the terms and conditions of the license of that
+ module.  An independent module is a module which is not derived from
+ or based on this library.  If you modify this library, you may extend
+ this exception to your version of the library, but you are not
+ obligated to do so.  If you do not wish to do so, delete this
+ exception statement from your version.
+ */
+package net.sourceforge.jnlp.runtime;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import net.sourceforge.jnlp.InformationDesc;
+import net.sourceforge.jnlp.JNLPFile;
+import net.sourceforge.jnlp.cache.UpdatePolicy;
+import net.sourceforge.jnlp.mock.DummyJNLPFileWithJar;
+import net.sourceforge.jnlp.util.FileTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class JNLPFileTest {
+
+    @Test
+    public void removeTitle() throws Exception {
+        File tempDirectory = FileTestUtils.createTempDirectory();
+        tempDirectory.deleteOnExit();
+        File jarLocation1 = new File(tempDirectory, "test1.jar");
+        File jarLocation2 = new File(tempDirectory, "test2.jar");
+        File jarLocation3 = new File(tempDirectory, "test3.jar");
+        File jarLocation4 = new File(tempDirectory, "test4.jar");
+        File jarLocation5 = new File(tempDirectory, "test5.jar");
+
+        /* Test with various attributes in manifest!s! */
+        Manifest manifest1 = new Manifest();
+        manifest1.getMainAttributes().put(Attributes.Name.MAIN_CLASS, "DummyClass1"); //two times, but one in main jar, see DummyJNLPFileWithJar constructor with int
+
+        Manifest manifest2 = new Manifest();
+        manifest2.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_VENDOR, "rh1"); //two times, both in not main jar, see DummyJNLPFileWithJar constructor with int
+
+        Manifest manifest3 = new Manifest();
+        manifest3.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_TITLE, "it"); //jsut once in not main jar, see DummyJNLPFileWithJar constructor with int
+        manifest3.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_VENDOR, "rh2");
+
+        Manifest manifest4 = new Manifest();
+        manifest4.getMainAttributes().put(Attributes.Name.MAIN_CLASS, "DummyClass2"); //see jnlpFile.setMainJar(3);
+        manifest4.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_URL, "some url2"); //see DummyJNLPFileWithJar constructor with int
+
+        //first jar
+        Manifest manifest5 = new Manifest();
+        manifest5.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_URL, "some url1"); //see DummyJNLPFileWithJar constructor with int
+        manifest5.getMainAttributes().put(new Attributes.Name(JNLPFile.APP_NAME), "Manifested Name");
+
+
+        FileTestUtils.createJarWithContents(jarLocation1, manifest1);
+        FileTestUtils.createJarWithContents(jarLocation2, manifest2);
+        FileTestUtils.createJarWithContents(jarLocation3, manifest3);
+        FileTestUtils.createJarWithContents(jarLocation4, manifest4);
+        FileTestUtils.createJarWithContents(jarLocation5, manifest5);
+
+        final DummyJNLPFileWithJar jnlpFile = new DummyJNLPFileWithJar(3, jarLocation5, jarLocation3, jarLocation4, jarLocation1, jarLocation2); //jar 1 should be main
+        Assert.assertNull("no classlaoder attached, should be null", jnlpFile.getManifestsAttributes().getMainClass());
+        Assert.assertNull("no classlaoder attached, should be null", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.IMPLEMENTATION_VENDOR));
+        Assert.assertNull("no classlaoder attached, should be null", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.IMPLEMENTATION_TITLE));
+        Assert.assertNull("no classlaoder attached, should be null", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.MAIN_CLASS));
+        Assert.assertNull("no classlaoder attached, should be null", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.IMPLEMENTATION_VENDOR_ID));
+        Assert.assertNull("no classlaoder attached, should be null", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.IMPLEMENTATION_URL));
+        Assert.assertNull("no classlaoder attached, should be null", jnlpFile.getManifestsAttributes().getAttribute(new Attributes.Name(JNLPFile.APP_NAME)));
+
+        Assert.assertNull(jnlpFile.getTitleFromJnlp());
+        Assert.assertNull(jnlpFile.getTitleFromManifest());
+        Assert.assertNull(jnlpFile.getTitle());
+
+        setTitle(jnlpFile);
+
+        Assert.assertEquals("jnlp title", jnlpFile.getTitleFromJnlp());
+        Assert.assertNull(jnlpFile.getTitleFromManifest());
+        Assert.assertEquals("jnlp title", jnlpFile.getTitle());
+
+        removeTitle(jnlpFile);
+
+        Assert.assertNull(jnlpFile.getTitleFromJnlp());
+        Assert.assertNull(jnlpFile.getTitleFromManifest());
+        Assert.assertNull(jnlpFile.getTitle());
+
+        final JNLPClassLoader classLoader = new JNLPClassLoader(jnlpFile, UpdatePolicy.ALWAYS);
+
+        // thsi si strange, but not part of this test
+        // Assert.assertNotNull("classlaoder attached, should be not null", jnlpFile.getManifestsAttributes().getMainClass());
+        Assert.assertNull("defined twice, shoud be null", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.IMPLEMENTATION_VENDOR));
+        Assert.assertNotNull("classlaoder attached, should be not null", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.IMPLEMENTATION_TITLE));
+        Assert.assertNotNull("classlaoder attached, should be not null", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.MAIN_CLASS));
+        Assert.assertNull("not deffined, should benull", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.IMPLEMENTATION_VENDOR_ID));
+        Assert.assertNotNull("classlaoder attached, should be not null", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.IMPLEMENTATION_URL));
+        Assert.assertNotNull("classlaoder attached, should be not null", jnlpFile.getManifestsAttributes().getAttribute(new Attributes.Name(JNLPFile.APP_NAME)));
+        //correct values are also tested in JnlpClassloaderTest
+        Assert.assertEquals("classlaoder attached, should be not null", "it", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.IMPLEMENTATION_TITLE));
+        Assert.assertEquals("classlaoder attached, should be not null", "DummyClass1", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.MAIN_CLASS));
+        Assert.assertEquals("classlaoder attached, should be not null", "some url1", jnlpFile.getManifestsAttributes().getAttribute(Attributes.Name.IMPLEMENTATION_URL));
+        Assert.assertEquals("classlaoder attached, should be not null", "Manifested Name", jnlpFile.getManifestsAttributes().getAttribute(new Attributes.Name(JNLPFile.APP_NAME)));
+
+        Assert.assertNull(jnlpFile.getTitleFromJnlp());
+        Assert.assertEquals("Manifested Name", jnlpFile.getTitleFromManifest());
+        Assert.assertEquals("Manifested Name", jnlpFile.getTitle());
+
+        setTitle(jnlpFile);
+
+        Assert.assertEquals("jnlp title", jnlpFile.getTitleFromJnlp());
+        Assert.assertEquals("Manifested Name", jnlpFile.getTitleFromManifest());
+        Assert.assertEquals("jnlp title (Manifested Name)", jnlpFile.getTitle());
+
+    }
+
+    private void setTitle(final DummyJNLPFileWithJar jnlpFile) {
+        setTitle(jnlpFile, "jnlp title");
+    }
+
+    private void setTitle(final DummyJNLPFileWithJar jnlpFile, final String title) {
+        jnlpFile.setInfo(Arrays.asList(new InformationDesc[]{
+                    new InformationDesc(new Locale[]{}) {
+                        @Override
+                        public String getTitle() {
+                            return title;
+                        }
+                    }
+                }));
+    }
+
+    private void removeTitle(final DummyJNLPFileWithJar jnlpFile) {
+        jnlpFile.setInfo(Arrays.asList(new InformationDesc[]{}));
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/netx/unit/net/sourceforge/jnlp/runtime/ResourcesDescTest.java	Wed Nov 13 11:15:18 2013 +0100
@@ -0,0 +1,105 @@
+/* 
+ Copyright (C) 2013 Red Hat, Inc.
+
+ This file is part of IcedTea.
+
+ IcedTea is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, version 2.
+
+ IcedTea is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with IcedTea; see the file COPYING.  If not, write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ 02110-1301 USA.
+
+ Linking this library statically or dynamically with other modules is
+ making a combined work based on this library.  Thus, the terms and
+ conditions of the GNU General Public License cover the whole
+ combination.
+
+ As a special exception, the copyright holders of this library give you
+ permission to link this library with independent modules to produce an
+ executable, regardless of the license terms of these independent
+ modules, and to copy and distribute the resulting executable under
+ terms of your choice, provided that you also meet, for each linked
+ independent module, the terms and conditions of the license of that
+ module.  An independent module is a module which is not derived from
+ or based on this library.  If you modify this library, you may extend
+ this exception to your version of the library, but you are not
+ obligated to do so.  If you do not wish to do so, delete this
+ exception statement from your version.
+ */
+package net.sourceforge.jnlp.runtime;
+
+import java.io.File;
+import java.util.jar.Manifest;
+import net.sourceforge.jnlp.JARDesc;
+import net.sourceforge.jnlp.mock.DummyJNLPFileWithJar;
+import net.sourceforge.jnlp.util.FileTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ResourcesDescTest {
+
+    @Test
+    public void checkGetMainJar_noMainSet() throws Exception {
+        File tempDirectory = FileTestUtils.createTempDirectory();
+        tempDirectory.deleteOnExit();
+
+        File jarLocation1 = new File(tempDirectory, "test1.jar");
+        File jarLocation2 = new File(tempDirectory, "test2.jar");
+        File jarLocation3 = new File(tempDirectory, "test3.jar");
+
+        Manifest manifest1 = new Manifest();
+        Manifest manifest2 = new Manifest();
+        Manifest manifest3 = new Manifest();
+        Manifest manifest4 = new Manifest();
+        Manifest manifest5 = new Manifest();
+
+        FileTestUtils.createJarWithContents(jarLocation1, manifest1);
+        FileTestUtils.createJarWithContents(jarLocation2, manifest2);
+        FileTestUtils.createJarWithContents(jarLocation3, manifest3);
+
+        final DummyJNLPFileWithJar jnlpFile = new DummyJNLPFileWithJar(jarLocation1, jarLocation2, jarLocation3);
+        JARDesc result = jnlpFile.getResources().getMainJAR();
+        Assert.assertTrue("first jar must be returned", result.getLocation().getFile().endsWith("test1.jar"));
+    }
+
+    @Test
+    public void checkGetMainJar_mainSet() throws Exception {
+        File tempDirectory = FileTestUtils.createTempDirectory();
+        tempDirectory.deleteOnExit();
+
+        File jarLocation1 = new File(tempDirectory, "test1.jar");
+        File jarLocation2 = new File(tempDirectory, "test2.jar");
+        File jarLocation3 = new File(tempDirectory, "test3.jar");
+
+        Manifest manifest1 = new Manifest();
+        Manifest manifest2 = new Manifest();
+        Manifest manifest3 = new Manifest();
+        Manifest manifest4 = new Manifest();
+        Manifest manifest5 = new Manifest();
+
+        FileTestUtils.createJarWithContents(jarLocation1, manifest1);
+        FileTestUtils.createJarWithContents(jarLocation2, manifest2);
+        FileTestUtils.createJarWithContents(jarLocation3, manifest3);
+
+        DummyJNLPFileWithJar jnlpFile = new DummyJNLPFileWithJar(0, jarLocation1, jarLocation2, jarLocation3);
+        JARDesc result = jnlpFile.getResources().getMainJAR();
+        Assert.assertTrue("main jar must be returned", result.getLocation().getFile().endsWith("test1.jar"));
+
+        jnlpFile = new DummyJNLPFileWithJar(1, jarLocation1, jarLocation2, jarLocation3);
+        result = jnlpFile.getResources().getMainJAR();
+        Assert.assertTrue("main jar must be returned", result.getLocation().getFile().endsWith("test2.jar"));
+
+        jnlpFile = new DummyJNLPFileWithJar(2, jarLocation1, jarLocation2, jarLocation3);
+        result = jnlpFile.getResources().getMainJAR();
+        Assert.assertTrue("main jar must be returned", result.getLocation().getFile().endsWith("test3.jar"));
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-extensions/net/sourceforge/jnlp/mock/DummyJNLPFileWithJar.java	Wed Nov 13 11:15:18 2013 +0100
@@ -0,0 +1,100 @@
+package net.sourceforge.jnlp.mock;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import net.sourceforge.jnlp.InformationDesc;
+import net.sourceforge.jnlp.JARDesc;
+import net.sourceforge.jnlp.JNLPFile;
+import net.sourceforge.jnlp.ResourcesDesc;
+import net.sourceforge.jnlp.SecurityDesc;
+import net.sourceforge.jnlp.Version;
+
+/* A mocked dummy JNLP file with a single JAR. */
+public class DummyJNLPFileWithJar extends JNLPFile {
+
+    /* Create a JARDesc for the given URL location */
+    static JARDesc makeJarDesc(URL jarLocation, boolean main) {
+        return new JARDesc(jarLocation, new Version("1"), null, false,main, false,false);
+    }
+
+    private final JARDesc[] jarDescs;
+    private final File[] jarFiles;
+
+    public DummyJNLPFileWithJar(File... jarFiles) throws MalformedURLException {
+        this(-1, jarFiles);
+    }
+    public DummyJNLPFileWithJar(int main, File... jarFiles) throws MalformedURLException {
+        codeBase = jarFiles[0].getParentFile().toURI().toURL();
+        this.jarFiles = jarFiles;
+        jarDescs = new JARDesc[jarFiles.length];
+
+        for (int i = 0; i < jarFiles.length; i++) {
+            jarDescs[i] = makeJarDesc(jarFiles[i].toURI().toURL(), i==main);
+
+        }
+        info = new ArrayList<InformationDesc>();
+    }
+
+    public URL getJarLocation() {
+        try {
+            return jarFiles[0].toURI().toURL();
+        } catch (MalformedURLException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    
+    public URL getJarLocation(int i) {
+        try {
+            return jarFiles[i].toURI().toURL();
+        } catch (MalformedURLException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public JARDesc[] getJarDescs() {
+        return jarDescs;
+    }
+    
+    public JARDesc getJarDesc() {
+        return jarDescs[0];
+    }
+
+    public JARDesc getJarDesc(int i) {
+        return jarDescs[i];
+    }
+    
+        
+    @Override
+    public ResourcesDesc getResources() {
+        ResourcesDesc localResources = new ResourcesDesc(null, new Locale[0], new String[0], new String[0]);
+        for (JARDesc j : jarDescs) {
+            localResources.addResource(j);            
+        }
+        return localResources;
+    }
+    @Override
+    public ResourcesDesc[] getResourcesDescs(final Locale locale, final String os, final String arch) {
+        return new ResourcesDesc[] { getResources() };
+    }
+
+    @Override
+    public URL getCodeBase() {
+        return codeBase;
+    }
+
+    @Override
+    public SecurityDesc getSecurity() {
+        return new SecurityDesc(this, SecurityDesc.SANDBOX_PERMISSIONS, null);
+    }
+
+    public void setInfo(List<InformationDesc> info) {
+        this.info = info;
+    }
+    
+    
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-extensions/net/sourceforge/jnlp/util/FileTestUtils.java	Wed Nov 13 11:15:18 2013 +0100
@@ -0,0 +1,129 @@
+/*
+Copyright (C) 2013 Red Hat, Inc.
+
+This file is part of IcedTea.
+
+IcedTea is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 2.
+
+IcedTea is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with IcedTea; see the file COPYING.  If not, write to
+the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301 USA.
+
+Linking this library statically or dynamically with other modules is
+making a combined work based on this library.  Thus, the terms and
+conditions of the GNU General Public License cover the whole
+combination.
+
+As a special exception, the copyright holders of this library give you
+permission to link this library with independent modules to produce an
+executable, regardless of the license terms of these independent
+modules, and to copy and distribute the resulting executable under
+terms of your choice, provided that you also meet, for each linked
+independent module, the terms and conditions of the license of that
+module.  An independent module is a module which is not derived from
+or based on this library.  If you modify this library, you may extend
+this exception to your version of the library, but you are not
+obligated to do so.  If you do not wish to do so, delete this
+exception statement from your version.
+ */
+
+package net.sourceforge.jnlp.util;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.management.ManagementFactory;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import net.sourceforge.jnlp.ServerAccess;
+
+public class FileTestUtils {
+
+    /* Get the open file-descriptor count for the process. Note that this is
+     * specific to Unix-like operating systems. */
+    static public long getOpenFileDescriptorCount() {
+        MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer();
+        try {
+            return (Long) beanServer.getAttribute(new ObjectName(
+                    "java.lang:type=OperatingSystem"),
+                    "OpenFileDescriptorCount");
+        } catch (Exception e) {
+            // Effectively disables leak tests
+            ServerAccess.logErrorReprint("Warning: Cannot get file descriptors for this platform!");
+            return 0;
+        }
+    }
+
+    /* Check the amount of file descriptors before and after a Runnable */
+    static public void assertNoFileLeak(Runnable runnable) {
+        long filesOpenBefore = getOpenFileDescriptorCount();
+        runnable.run();
+        long filesLeaked = getOpenFileDescriptorCount() - filesOpenBefore;
+        assertEquals(0, filesLeaked);
+    }
+
+    /* Creates a file with the given contents */
+    static public void createFileWithContents(File file, String contents)
+            throws IOException {
+        PrintWriter out = new PrintWriter(file);
+        out.write(contents);
+        out.close();
+    }
+
+    /* Creates a jar in a temporary directory, with the given name & file contents */
+    static public void createJarWithContents(File jarFile, Manifest manifestContents, File... fileContents)
+            throws Exception {
+        /* Manifest quite evilly ignores all attributes if we don't specify a version! 
+         * Make sure it's set here. */
+        manifestContents.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+
+        JarOutputStream jarWriter = new JarOutputStream(new FileOutputStream(jarFile), manifestContents);
+        for (File file : fileContents) {
+            jarWriter.putNextEntry(new JarEntry(file.getName()));
+            FileInputStream fileReader = new FileInputStream(file);
+            StreamUtils.copyStream(fileReader, jarWriter);
+            fileReader.close();
+            jarWriter.closeEntry();
+        }
+        jarWriter.close();
+    }
+
+    /* Creates a jar in a temporary directory, with the given name, manifest & file contents */
+    static public void createJarWithContents(File jarFile, File... fileContents) throws Exception {
+        /* Note that we always specify a manifest, to avoid empty jars.
+         * Empty jars are not allowed by icedtea-web during the zip-file header check. */
+        createJarWithContents(jarFile, new Manifest(), fileContents);
+    }
+
+    /* Creates a temporary directory. Note that Java 7 has a method for this,
+     * but we want to remain 6-compatible. */
+    static public File createTempDirectory() throws IOException {
+        File file = File.createTempFile("temp",
+                Long.toString(System.nanoTime()));
+        file.delete();
+        if (!file.mkdir()) {
+            throw new IOException("Failed to create temporary directory '"
+                    + file + "' for test.");
+        }
+        return file;
+    }
+
+}