/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.model;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.Util;
import hudson.util.IOUtils;
import hudson.util.QuotedStringTokenizer;
import hudson.util.TextFile;
import hudson.util.TimeUnit2;
import org.kohsuke.stapler.Stapler;
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(Stapler.getCurrentRequest().getContextPath()+'/'+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.java.net/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()+"updates/"+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");
}