/******************************************************************************* * * Copyright (c) 2004-2009 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi * * *******************************************************************************/ package hudson.model; import hudson.Extension; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.Functions; import hudson.Util; import hudson.util.IOUtils; import hudson.util.QuotedStringTokenizer; import hudson.util.TextFile; import hudson.util.TimeUnit2; import java.io.File; import java.io.IOException; import java.util.logging.Logger; import net.sf.json.JSONObject; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; /** * Service for plugins to periodically retrieve update data files (like the one * in the update center) through browsers. * * <p> Because the retrieval of the file goes through XmlHttpRequest, we cannot * reliably pass around binary. * * @author Kohsuke Kawaguchi */ @Extension public class DownloadService extends PageDecorator { public DownloadService() { super(DownloadService.class); } /** * Builds up an HTML fragment that starts all the download jobs. */ public String generateFragment() { if (neverUpdate) { return ""; } StringBuilder buf = new StringBuilder(); if (Hudson.getInstance().hasPermission(Hudson.READ)) { long now = System.currentTimeMillis(); for (Downloadable d : Downloadable.all()) { if (d.getDue() < now && d.lastAttempt + 10 * 1000 < now) { buf.append("<script>") .append("Behaviour.addLoadEvent(function() {") .append(" downloadService.download(") .append(QuotedStringTokenizer.quote(d.getId())) .append(',') .append(QuotedStringTokenizer.quote(d.getUrl())) .append(',') .append("{version:") .append(QuotedStringTokenizer.quote(Hudson.VERSION + "-" + Util.getDigestOf(Hudson.getInstance().getSecretKey()))) .append('}') .append(',') .append(QuotedStringTokenizer.quote(Functions.getRequestRootPath() + '/' + getUrl() + "/byId/" + d.getId() + "/postBack")) .append(',') .append("null);") .append("});") .append("</script>"); d.lastAttempt = now; } } } return buf.toString(); } /** * Gets {@link Downloadable} by its ID. Used to bind them to URL. */ public Downloadable getById(String id) { for (Downloadable d : Downloadable.all()) { if (d.getId().equals(id)) { return d; } } return null; } /** * Represents a periodically updated JSON data file obtained from a remote * URL. * * <p> This mechanism is one of the basis of the update center, which * involves fetching up-to-date data file. * * @since 1.305 */ public static class Downloadable implements ExtensionPoint { private final String id; private final String url; private final long interval; private volatile long due = 0; private volatile long lastAttempt = Long.MIN_VALUE; /** * * @param url URL relative to {@link UpdateCenter#getUrl()}. So if this * string is "foo.json", the ultimate URL will be something like * "https://hudson-ci.org/update-center3.3.2/foo.json" * * For security and privacy reasons, we don't allow the retrieval from * random locations. */ public Downloadable(String id, String url, long interval) { this.id = id; this.url = url; this.interval = interval; } /** * Uses the class name as an ID. */ public Downloadable(Class id) { this(id.getName().replace('$', '.')); } public Downloadable(String id) { this(id, id + ".json"); } public Downloadable(String id, String url) { this(id, url, TimeUnit2.DAYS.toMillis(1)); } public String getId() { return id; } /** * URL to download. */ public String getUrl() { return Hudson.getInstance().getUpdateCenter().getDefaultBaseUrl() + url; } /** * How often do we retrieve the new image? * * @return number of milliseconds between retrieval. */ public long getInterval() { return interval; } /** * This is where the retrieved file will be stored. */ public TextFile getDataFile() { return new TextFile(new File(Hudson.getInstance().getRootDir(), "updates/" + id)); } /** * When shall we retrieve this file next time? */ public long getDue() { if (due == 0) // if the file doesn't exist, this code should result // in a very small (but >0) due value, which should trigger // the retrieval immediately. { due = getDataFile().file.lastModified() + interval; } return due; } /** * Loads the current file into JSON and returns it, or null if no data * exists. */ public JSONObject getData() throws IOException { TextFile df = getDataFile(); if (df.exists()) { return JSONObject.fromObject(df.read()); } return null; } /** * This is where the browser sends us the data. */ public void doPostBack(StaplerRequest req, StaplerResponse rsp) throws IOException { long dataTimestamp = System.currentTimeMillis(); TextFile df = getDataFile(); df.write(IOUtils.toString(req.getInputStream(), "UTF-8")); df.file.setLastModified(dataTimestamp); due = dataTimestamp + getInterval(); LOGGER.info("Obtained the updated data file for " + id); rsp.setContentType("text/plain"); // So browser won't try to parse response } /** * Returns all the registered {@link Downloadable}s. */ public static ExtensionList<Downloadable> all() { return Hudson.getInstance().getExtensionList(Downloadable.class); } /** * Returns the {@link Downloadable} that has the given ID. */ public static Downloadable get(String id) { for (Downloadable d : all()) { if (d.id.equals(id)) { return d; } } return null; } private static final Logger LOGGER = Logger.getLogger(Downloadable.class.getName()); } public static boolean neverUpdate = Boolean.getBoolean(DownloadService.class.getName() + ".never"); }