views:

192

answers:

6

For the past couple of months I've been working on a game in java for a university project. It's coming up to the end of the project and I would like to compile the project into a single file which is easy to distribute. The game currently runs from inside the IDE and relies on the working directory being set somewhere specific (i.e. the content directory with sounds/textures etc). What's the best way to put all this together for portability? I'm hoping there is some way to compile the content into the jar file...

NB. I'm using NetBeans, so any solutions which are easy in netbeans get extra credit ;)

Addendum::

For future reference, I found a way of accessing things by directory, ythis may not be the best way but it works:

File directory = new File(ClassLoader.getSystemResource("fullprototypeone/Content/Levels/").toURI());

And now I can just use that file object as normal

+1  A: 

Here is the manual for JNLP and Java Web Start. These technologies exist just for the task you've described.

Roman
+1  A: 

Well one way is to access the resources via Class.getResourceAsStream (or Class.getResource). Then make sure that the files are in the JAR file.

Off the top of my head (and without trying it) you should be able to put the resources in with the source files which will get NetBeans to put them into the JAR file. Then change the File stuff to the getResource calls.

I would suggest making a simple program that plays a sound and trying it out before you try to convert the whole project over.

If you try and it doesn't work let me know and I'll see if I can dig into it a bit more (posting the code of the simple project would be good if it comes to that).

TofuBeer
+6  A: 

You can embed resources in jar file - this is just a zip after all. The standard way to do that is to put resource files in some directory in your sources hierachy. Then you refer to them by Object.getClass().getResourceAsStream(). So you will need to change the way you retrieve them in your code.

You can read more here: Object.getClass().getResourceAsStream(). Of course instead of object you use some class from your package.

when you put those resource files in your src hierachy I believe Netbeans should jar them for you with standard build of the project.

pajton
This has worked for all things which directly use individual files, is there a way to treat a package as a directory and scan all files in it? This is the current brick wall I have hit.
Martin
@Martin Yes, but it isn't trivial to come up with. I'll see if I can find some code I have that does it (pretty sure I can... but will be tomorrow).
TofuBeer
You could use JarFile for that, though as TofuBeer said it is not so simple. I'd suggest something else: while building a project you could precompute a list of resources and embed it as well in a file, for example filelist.properties. The you read this file and know all resources you have.
pajton
That sounds like a viable approach, any advice on how to set up such a custom build step?
Martin
Well, it depends whether you are using Ant or maven build (I believe Netbeans uses Ant by default). So, you could write this as an Ant task.
pajton
A: 

yes ! put your compiled .class files and your resources with folders in a jar file .. you ll have to build a manifest file as well .. you can find the tutorial about making a .jar file on google. most probably you ll be referred to java.sun.com .

sp3tsnaz
A: 

I have an answer for a similar 'classes and content' from NetBeans question. It may help you, too.

vkraemer
+1  A: 

Here is the code I promised in another comment... it isn't quite what I remember but it might get you started.

Essentially you call: String fileName = FileUtils.getFileName(Main.class, "foo.txt");

and it goes and finds that file on disk or in a JAR file. If it is in the JAR file it extracts it to a temp directory. You can then use "new File(fileName)" to open the file which, no matter where it was before, will be on the disk.

What I would do is take a look at the getFile method and look at what you can do with the JAR file to iterate over the contents of it and find the files in a given directory.

Like I said, not exactly what you want, but does do a lot of the initial work for you.

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;


public class FileUtils
{
    public static String getFileName(final Class<?>  owner,
                                     final String    name)
        throws URISyntaxException,
               ZipException,
               IOException
    {
        String    fileName;
        final URI uri;

        try
        {
            final String external;
            final String decoded;
            final int    pos;

            uri      = getResourceAsURI(owner.getPackage().getName().replaceAll("\\.", "/") + "/" + name, owner);
            external = uri.toURL().toExternalForm();
            decoded  = external; // URLDecoder.decode(external, "UTF-8");
            pos      = decoded.indexOf(":/");
            fileName = decoded.substring(pos + 1);
        }
        catch(final FileNotFoundException ex)
        {
            fileName = null;
        }

        if(fileName == null || !(new File(fileName).exists()))
        {
            fileName = getFileNameX(owner, name);
        }

        return (fileName);
    }

    private static String getFileNameX(final Class<?> clazz, final String name)
        throws UnsupportedEncodingException
    {
        final URL    url;
        final String fileName;

        url = clazz.getResource(name);

        if(url == null)
        {
            fileName = name;
        }
        else
        {
            final String decoded;
            final int    pos;

            decoded  = URLDecoder.decode(url.toExternalForm(), "UTF-8");
            pos      = decoded.indexOf(":/");
            fileName = decoded.substring(pos + 1);
        }

        return (fileName);
    }

    private static URI getResourceAsURI(final String    resourceName,
                                       final Class<?> clazz)
        throws URISyntaxException,
               ZipException,
               IOException
    {
        final URI uri;
        final URI resourceURI;

        uri         = getJarURI(clazz);
        resourceURI = getFile(uri, resourceName);

        return (resourceURI);
    }

    private static URI getJarURI(final Class<?> clazz)
        throws URISyntaxException
    {
        final ProtectionDomain domain;
        final CodeSource       source;
        final URL              url;
        final URI              uri;

        domain = clazz.getProtectionDomain();
        source = domain.getCodeSource();
        url    = source.getLocation();
        uri    = url.toURI();

        return (uri);
    }

    private static URI getFile(final URI    where,
                               final String fileName)
        throws ZipException,
               IOException
    {
        final File location;
        final URI  fileURI;

        location = new File(where);

        // not in a JAR, just return the path on disk
        if(location.isDirectory())
        {
            fileURI = URI.create(where.toString() + fileName);
        }
        else
        {
            final ZipFile zipFile;

            zipFile = new ZipFile(location);

            try
            {
                fileURI = extract(zipFile, fileName);
            }
            finally
            {
                zipFile.close();
            }
        }

        return (fileURI);
    }

    private static URI extract(final ZipFile zipFile,
                               final String  fileName)
        throws IOException
    {
        final File         tempFile;
        final ZipEntry     entry;
        final InputStream  zipStream;
        OutputStream       fileStream;

        tempFile = File.createTempFile(fileName.replace("/", ""), Long.toString(System.currentTimeMillis()));
        tempFile.deleteOnExit();
        entry    = zipFile.getEntry(fileName);

        if(entry == null)
        {
            throw new FileNotFoundException("cannot find file: " + fileName + " in archive: " + zipFile.getName());
        }

        zipStream  = zipFile.getInputStream(entry);
        fileStream = null;

        try
        {
            final byte[] buf;
            int          i;

            fileStream = new FileOutputStream(tempFile);
            buf        = new byte[1024];
            i          = 0;

            while((i = zipStream.read(buf)) != -1)
            {
                fileStream.write(buf, 0, i);
            }
        }
        finally
        {
            close(zipStream);
            close(fileStream);
        }

        return (tempFile.toURI());
    }

    private static void close(final Closeable stream)
    {
        if(stream != null)
        {
            try
            {
                stream.close();
            }
            catch(final IOException ex)
            {
                ex.printStackTrace();
            }
        }
    }
}

Edit:

Sorry, I don't have time to work on this right now (if I get some I'll do it and post the code here). This is what I would do though:

  1. Look at the ZipFile class and use the entries() method to find all of the files/directories in the zip file.
  2. the ZipEntry has an isDirectory() method that you can use to figure out what it is.
  3. I think the code I posted in this answer will give you a way to pick a temporary directory to extract the contents to.
  4. I think the code I posted in this answer could help with copying the ZipEntry contents to the file system.
  5. once the items are on the file system the code you already have for iterating over the directory would still work. You would add a new method to the FileUtils class in the code above and be able to find all of the files as you are doing now.

There is probably a better way to do it, but off the top of my head I think that will work.

TofuBeer
so what's wrong the technique I appended to my answer? :/
Martin
1) I didn't see it (I just woke up!)2) won't that return an odd URL with JAR! in it that you cannot open as a file?
TofuBeer
I'm afraid this is beyond my understanding of java Jars files, can you give me a little more help with this one?
Martin
probably. let me jsut make sure I understand what you are after... 1) whole app (classes and resources) in a single JAR file. 2) you have to be able to find out what files are in a directory. 3) you don't care if they are extracted at runtime or not, just as long as all you send off is the single JAR file. Correct?
TofuBeer
Yes, I just want the app in a single jar file to distribute, if the jar file extracts a load of files (just once, or every time it is run) I don't care
Martin
Sorry for the delay... I am deep into some code right now and just don't have the time to do it... I'll update my answer to give some hints as to what to do.
TofuBeer