-1

I am writing a simple (generic) wrapper Java class that will execute on various computers separate from a deployed web server. I want to download the latest version of a jar file that is the application from that associated Web Server (currently Jetty 8).

I have code like this:

// Get the jar URL which contains the application
URL jarFileURL = new URL("jar:http://localhost:8081/myapplication.jar!/");
JarURLConnection jcl = (JarURLConnection) jarFileURL.openConnection();

Attributes attr = jcl.getMainAttributes();
String mainClass = (attr != null)
            ? attr.getValue(Attributes.Name.MAIN_CLASS)
            : null;
if (mainClass != null)  // launch the program

This works well, except that myapplication.jar is a large jar file (a OneJar jarfile, so a lot is in there). I would like this to be as efficient as possible. The jar file isn't going to change very often.

  1. Can the jar file be saved to disk (I see how to get a JarFile object, but not to save it)?
  2. More importantly, but related to #1, can the jar file be cached somehow?

    2.1 can I (easily) request the MD5 of the jar file on the web server and only download it when that has changed?
    2.2 If not is there another caching mechanism, maybe request only the Manifest? Version/Build info could be stored there.

If anyone done something similar could you sketch out in as much detail what to do?

UPDATES PER INITIAL RESPONSES

The suggestion is to use an If-Modified-Since header in the request and the openStream method on the URL to get the jar file to save.

Based on this feedback, I have added one critical piece of info and some more focused questions.

The java program I am describing above runs the program downloaded from the jar file referenced. This program will run from around 30 seconds to maybe 5 minutes or so. Then it is done and exits. Some user may run this program multiple times per day (say even up to 100 times), others may run it as infrequently as once every other week. It should still be smart enough to know if it has the most current version of the jar file.

More Focused Questions:

Will the If-Modified-Since header still work in this usage? If so, will I need completely different code to add that? That is, can you show me how to modify the code presented to include that? Same question with regard to saving the jar file - ultimately I am really surprised (frustrated!) that I can get a JarFile object, but have no way to persist it - will I even need the JarURLConnection class?

Bounty Question

I didn't initially realize the precise question I was trying to ask. It is this:

How can I save a jar file from a web server locally in a command-line program that exits and ONLY update that jar file when it has been changed on the server?

Any answer that, via code examples, shows how that may be done will be awarded the bounty.

user207421
  • 305,947
  • 44
  • 307
  • 483
JoeG
  • 7,191
  • 10
  • 60
  • 105
  • 2
    You can use an `If-Modified-Since` header in your GET request, and the server will answer 304 if the file has not changed – fge Jan 07 '15 at 17:35
  • Why not just use the DefaultServlet to serve the files from the Jetty server, and then on the client side pass in the current file-system's lastModified timestamp as the HTTP request header `If-Modified-Since`. No point reinventing the wheel on this one. – Joakim Erdfelt Jan 07 '15 at 23:24
  • @Joakim Erdfelt - that sounds all well and good, but I totally don't know how to do that! Have you a pointer to an article or another SO question? Thanks! – JoeG Jan 08 '15 at 00:47
  • Your question isn't clear. Even your updates and Bounty Question are not clear. – Joakim Erdfelt Jan 14 '15 at 18:52
  • to translate: "save a jar file from a web server locally" - you have a webserver on localhost and want to download a file from that localhost webserver, but only when it changes - why? its local. just copy the file using the filesystem. – Joakim Erdfelt Jan 14 '15 at 18:53

4 Answers4

1
  1. Yes, the file can be saved to the disk, you can get the input stream using the method openStream() in URL class.

  2. As per the comment mentioned by @fge there is a way to detect whether the file is modified.

Sample Code:

private void launch() throws IOException {
    // Get the jar URL which contains the application
    String jarName = "myapplication.jar";
    String strUrl = "jar:http://localhost:8081/" + jarName + "!/";

    Path cacheDir = Paths.get("cache");
    Files.createDirectories(cacheDir);
    Path fetchUrl = fetchUrl(cacheDir, jarName, strUrl);
    JarURLConnection jcl = (JarURLConnection) fetchUrl.toUri().toURL().openConnection();

    Attributes attr = jcl.getMainAttributes();
    String mainClass = (attr != null) ? attr.getValue(Attributes.Name.MAIN_CLASS) : null;
    if (mainClass != null) {
        // launch the program
    }
}

private Path fetchUrl(Path cacheDir, String title, String strUrl) throws IOException {
    Path cacheFile = cacheDir.resolve(title);
    Path cacheFileDate = cacheDir.resolve(title + "_date");
    URL url = new URL(strUrl);
    URLConnection connection = url.openConnection();
    if (Files.exists(cacheFile) && Files.exists(cacheFileDate)) {
        String dateValue = Files.readAllLines(cacheFileDate).get(0);
        connection.addRequestProperty("If-Modified-Since", dateValue);

        String httpStatus = connection.getHeaderField(0);
        if (httpStatus.indexOf(" 304 ") == -1) { // assuming that we get status 200 here instead
            writeFiles(connection, cacheFile, cacheFileDate);
        } else { // else not modified, so do not do anything, we return the cache file
            System.out.println("Using cached file");
        }
    } else {
        writeFiles(connection, cacheFile, cacheFileDate);
    }

    return cacheFile;
}

private void writeFiles(URLConnection connection, Path cacheFile, Path cacheFileDate) throws IOException {
    System.out.println("Creating cache entry");
    try (InputStream inputStream = connection.getInputStream()) {
        Files.copy(inputStream, cacheFile, StandardCopyOption.REPLACE_EXISTING);
    }
    String lastModified = connection.getHeaderField("Last-Modified");
    Files.write(cacheFileDate, lastModified.getBytes());
    System.out.println(connection.getHeaderFields());
}
MJSG
  • 1,035
  • 8
  • 12
  • This is exactly what I was looking for - OBVIOUSLY I didn't understand the "If-Modified-Since"! Accordingly, I am giving this the bounty! I am still getting a couple of exceptions when I run it in a TestCase. Once I iron that out I will post what I needed to do. – JoeG Jan 15 '15 at 23:52
1

How can I save a jar file from a web server locally in a command-line program that exits and ONLY update that jar file when it has been changed on the server?

With JWS. It has an API so you can control it from your existing code. It already has versioning and caching, and comes with a JAR-serving servlet.

user207421
  • 305,947
  • 44
  • 307
  • 483
  • from the JWS link: "application-deployment technology that gives you the power to launch full-featured applications with a single click from your Web browser". I do not have a link in a browser. Given the failure of JWS to gain much traction after so many years of availability leaves me somewhat suspect as well. – JoeG Jan 12 '15 at 19:42
  • *Non sequitur.* You don't need a browser. As I stated, it has an API. Keep reading. – user207421 Jan 13 '15 at 21:49
  • Sorry - though at least to me, this is like saying: here is a link to Java: http://docs.oracle.com/javase/8/docs/api/index.html read it and tell me what you think. The FAQ is from March 2006 which was about the only time I tried using it. What is the difference between DownloadService and DownloadService2? Do I need a listener? When do I need to use the ExtendedService versus the BasicService? Is the PersistenceService the only one that will save the jars locally? So while I have used JWS a tiny bit MANY years ago, a general pointer to read the API (old? outdated?) doesn't really help – JoeG Jan 14 '15 at 13:01
1

I have assumed that a .md5 file will be available both locally and at the web server. Same logic will apply if you wanted this to be a version control file.

The urls given in the following code need to updated according to your web server location and app context. Here is how your command line code would go

public class Main {

public static void main(String[] args) {
    String jarPath = "/Users/nrj/Downloads/local/";
    String jarfile = "apache-storm-0.9.3.tar.gz";
    String md5File = jarfile + ".md5";

    try {
        // Update the URL to your real server location and application
        // context
        URL url = new URL(
                "http://localhost:8090/JarServer/myjar?hash=md5&file="
                        + URLEncoder.encode(jarfile, "UTF-8"));

        BufferedReader in = new BufferedReader(new InputStreamReader(
                url.openStream()));
        // get the md5 value from server
        String servermd5 = in.readLine();
        in.close();

        // Read the local md5 file
        in = new BufferedReader(new FileReader(jarPath + md5File));
        String localmd5 = in.readLine();
        in.close();

        // compare
        if (null != servermd5 && null != localmd5
                && localmd5.trim().equals(servermd5.trim())) {
            // TODO - Execute the existing jar
        } else {
            // Rename the old jar
            if (!(new File(jarPath + jarfile).renameTo((new File(jarPath + jarfile
                    + String.valueOf(System.currentTimeMillis())))))) {
                System.err
                        .println("Unable to rename old jar file.. please check write access");
            }
            // Download the new jar
            System.out
                    .println("New jar file found...downloading from server");
            url = new URL(
                    "http://localhost:8090/JarServer/myjar?download=1&file="
                            + URLEncoder.encode(jarfile, "UTF-8"));
            // Code to download
            byte[] buf;
            int byteRead = 0;
            BufferedOutputStream outStream = new BufferedOutputStream(
                    new FileOutputStream(jarPath + jarfile));

            InputStream is = url.openConnection().getInputStream();
            buf = new byte[10240];
            while ((byteRead = is.read(buf)) != -1) {
                outStream.write(buf, 0, byteRead);
            }
            outStream.close();
            System.out.println("Downloaded Successfully.");

            // Now update the md5 file with the new md5
            BufferedWriter bw = new BufferedWriter(new FileWriter(md5File));
            bw.write(servermd5);
            bw.close();

            // TODO - Execute the jar, its saved in the same path
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}
}

And just in case you had control over the servlet code as well, this is how the servlet code goes:-

@WebServlet(name = "jarervlet", urlPatterns = { "/myjar" })
public class JarServlet extends HttpServlet {

private static final long serialVersionUID = 1L;
// Remember to have a '/' at the end, otherwise code will fail
private static final String PATH_TO_FILES = "/Users/nrj/Downloads/";

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

    String fileName = req.getParameter("file");
    if (null != fileName) {
        fileName = URLDecoder.decode(fileName, "UTF-8");
    }
    String hash = req.getParameter("hash");
    if (null != hash && hash.equalsIgnoreCase("md5")) {
        resp.getWriter().write(readMd5Hash(fileName));
        return;
    }

    String download = req.getParameter("download");
    if (null != download) {
        InputStream fis = new FileInputStream(PATH_TO_FILES + fileName);
        String mimeType = getServletContext().getMimeType(
                PATH_TO_FILES + fileName);
        resp.setContentType(mimeType != null ? mimeType
                : "application/octet-stream");
        resp.setContentLength((int) new File(PATH_TO_FILES + fileName)
                .length());
        resp.setHeader("Content-Disposition", "attachment; filename=\""
                + fileName + "\"");

        ServletOutputStream os = resp.getOutputStream();
        byte[] bufferData = new byte[10240];
        int read = 0;
        while ((read = fis.read(bufferData)) != -1) {
            os.write(bufferData, 0, read);
        }
        os.close();
        fis.close();
        // Download finished
    }

}

private String readMd5Hash(String fileName) {
    // We are assuming there is a .md5 file present for each file
    // so we read the hash file to return hash
    try (BufferedReader br = new BufferedReader(new FileReader(
            PATH_TO_FILES + fileName + ".md5"))) {
        return br.readLine();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

}
NRJ
  • 1,064
  • 3
  • 15
  • 32
  • This is really good, it is just that the code provided for the first answer is virtually exactly what I was looking for. Thanks and I upvoted! – JoeG Jan 15 '15 at 23:50
0

I can share experience of solving the same problem in our team. We have several desktop product written in java which are updated regularly.

Couple years ago we had separate update server for every product and following process of update: Client application has an updater wrapper that starts before main logic, and stored in a udpater.jar. Before start, application send request to update server with MD5-hash of application.jar file. Server compares received hash with the one that it has, and send new jar file to updater if hashes are different.

But after many cases, where we confused which build is now in production, and update-server failures we switched to continuous integration practice with TeamCity on top of it.

Every commit done by developer is now tracked by build server. After compilation and test passing build server assigns build number to application and shares app distribution in local network.

Update server now is a simple web server with special structure of static files:

$WEB_SERVER_HOME/
   application-builds/
      987/
      988/
      989/
          libs/
          app.jar
          ...
          changes.txt  <- files, that changed from last build
      lastversion.txt  <- last build number

Updater on client side requests lastversion.txt via HttpClient, retrieves last build number and compares it with client build number stored in manifest.mf. If update is required, updater harvests all changes made since last update iterating over application-builds/$BUILD_NUM/changes.txt files. After that, updater downloads harvested list of files. There could be jar-files, config files, additional resources etc.

This scheme is seems complex for client updater, but in practice it is very clear and robust.

There is also a bash script that composes structure of files on updater server. Script request TeamCity every minute to get new builds and calculates diff between builds. We also upgrading now this solution to integrate with project management system (Redmine, Youtrack or Jira). The aim is to able product manager to mark build that are approved to be updated.

UPDATE.

I've moved our updater to github, check here: github.com/ancalled/simple-updater

Project contains updater-client on Java, server-side bash scripts (retrieves updates from build-server) and sample application to test updates on it.

ancalled
  • 174
  • 6
  • This certainly seems almost exactly like what I need. If you could share anything on a github repo that would help out a lot! – JoeG Jan 12 '15 at 19:46