package hudson.tools;
import hudson.FilePath;
import hudson.model.DownloadService.Downloadable;
import hudson.model.Node;
import hudson.model.TaskListener;
import hudson.slaves.NodeSpecific;
import net.sf.json.JSONObject;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.net.URL;
/**
* Partial convenience implementation of {@link ToolInstaller} that just downloads
* an archive from the URL and extracts it.
*
* <p>
* Each instance of this is configured to download from a specific URL identified by an ID.
*
* @author Kohsuke Kawaguchi
* @since 1.308
*/
public abstract class DownloadFromUrlInstaller extends ToolInstaller {
public final String id;
protected DownloadFromUrlInstaller(String id) {
// this installer implementation is designed for platform independent binary,
// and as such we don't provide the label support
super(null);
this.id = id;
}
/**
* Checks if the specified expected location already contains the installed version of the tool.
*
* This check needs to run fairly efficiently. The current implementation uses the souce URL of {@link Installable},
* based on the assumption that released bits do not change its content.
*/
protected boolean isUpToDate(FilePath expectedLocation, Installable i) throws IOException, InterruptedException {
FilePath marker = expectedLocation.child(".installedFrom");
return marker.exists() && marker.readToString().equals(i.url);
}
/**
* Gets the {@link Installable} identified by {@link #id}.
*
* @return null if no such ID is found.
*/
public Installable getInstallable() throws IOException {
for (Installable i : ((DescriptorImpl<?>)getDescriptor()).getInstallables())
if(id.equals(i.id))
return i;
return null;
}
public FilePath performInstallation(ToolInstallation tool, Node node, TaskListener log) throws IOException, InterruptedException {
FilePath expected = preferredLocation(tool, node);
Installable inst = getInstallable();
if(inst==null) {
log.getLogger().println("Invalid tool ID "+id);
return expected;
}
if (inst instanceof NodeSpecific) {
inst = (Installable) ((NodeSpecific) inst).forNode(node, log);
}
if(isUpToDate(expected,inst))
return expected;
if(expected.installIfNecessaryFrom(new URL(inst.url), log, "Unpacking " + inst.url + " to " + expected + " on " + node.getDisplayName())) {
expected.child(".timestamp").delete(); // we don't use the timestamp
FilePath base = findPullUpDirectory(expected);
if(base!=null && base!=expected)
base.moveAllChildrenTo(expected);
// leave a record for the next up-to-date check
expected.child(".installedFrom").write(inst.url,"UTF-8");
expected.act(new ZipExtractionInstaller.ChmodRecAPlusX());
}
return expected;
}
/**
* Often an archive contains an extra top-level directory that's unnecessary when extracted on the disk
* into the expected location. If your installation sources provide that kind of archives, override
* this method to find the real root location.
*
* <p>
* The caller will "pull up" the discovered real root by throw away the intermediate directory,
* so that the user-configured "tool home" directory contains the right files.
*
* <p>
* The default implementation applies some heuristics to auto-determine if the pull up is necessary.
* This should work for typical archive files.
*
* @param root
* The directory that contains the extracted archive. This directory contains nothing but the
* extracted archive. For example, if the user installed
* http://archive.apache.org/dist/ant/binaries/jakarta-ant-1.1.zip , this directory would contain
* a single directory "jakarta-ant".
*
* @return
* Return the real top directory inside {@code root} that contains the meat. In the above example,
* <tt>root.child("jakarta-ant")</tt> should be returned. If there's no directory to pull up,
* return null.
*/
protected FilePath findPullUpDirectory(FilePath root) throws IOException, InterruptedException {
// if the directory just contains one directory and that alone, assume that's the pull up subject
// otherwise leave it as is.
List<FilePath> children = root.list();
if(children.size()!=1) return null;
if(children.get(0).isDirectory())
return children.get(0);
return null;
}
public static abstract class DescriptorImpl<T extends DownloadFromUrlInstaller> extends ToolInstallerDescriptor<T> {
@SuppressWarnings("deprecation") // intentionally adding dynamic item here
protected DescriptorImpl() {
Downloadable.all().add(createDownloadable());
}
/**
* function that creates a {@link Downloadable}.
* @return a downloadable object
*/
public Downloadable createDownloadable() {
if (this instanceof DownloadFromUrlInstaller.DescriptorImpl) {
final DownloadFromUrlInstaller.DescriptorImpl delegate = (DownloadFromUrlInstaller.DescriptorImpl)this;
return new Downloadable(getId()) {
public JSONObject reduce(List<JSONObject> jsonList) {
if (isDefaultSchema(jsonList)) {
return delegate.reduce(jsonList);
} else {
//if it's not default schema fall back to the super class implementation
return super.reduce(jsonList);
}
}
};
}
return new Downloadable(getId());
}
/**
* this function checks is the update center tool has the default schema
* @param jsonList the list of Update centers json files
* @return true if the schema is the default one (id, name, url), false otherwise
*/
private boolean isDefaultSchema(List<JSONObject> jsonList) {
JSONObject jsonToolInstallerList = jsonList.get(0);
ToolInstallerList toolInstallerList = (ToolInstallerList) JSONObject.toBean(jsonToolInstallerList, ToolInstallerList.class);
if (toolInstallerList != null) {
ToolInstallerEntry[] entryList = toolInstallerList.list;
ToolInstallerEntry sampleEntry = entryList[0];
if (sampleEntry != null) {
if (sampleEntry.id != null && sampleEntry.name != null && sampleEntry.url != null) {
return true;
}
}
}
return false;
}
private JSONObject reduce(List<JSONObject> jsonList) {
List<ToolInstallerEntry> reducedToolEntries = new LinkedList<>();
//collect all tool installers objects from the multiple json objects
for (JSONObject jsonToolList : jsonList) {
ToolInstallerList toolInstallerList = (ToolInstallerList) JSONObject.toBean(jsonToolList, ToolInstallerList.class);
reducedToolEntries.addAll(Arrays.asList(toolInstallerList.list));
}
while (Downloadable.hasDuplicates(reducedToolEntries, "id")) {
List<ToolInstallerEntry> tmpToolInstallerEntries = new LinkedList<>();
//we need to skip the processed entries
boolean processed[] = new boolean[reducedToolEntries.size()];
for (int i = 0; i < reducedToolEntries.size(); i++) {
if (processed[i] == true) {
continue;
}
ToolInstallerEntry data1 = reducedToolEntries.get(i);
boolean hasDuplicate = false;
for (int j = i + 1; j < reducedToolEntries.size(); j ++) {
ToolInstallerEntry data2 = reducedToolEntries.get(j);
//if we found a duplicate we choose the first one
if (data1.id.equals(data2.id)) {
hasDuplicate = true;
processed[j] = true;
tmpToolInstallerEntries.add(data1);
//after the first duplicate has been found we break the loop since the duplicates are
//processed two by two
break;
}
}
//if no duplicate has been found we just insert the entry in the tmp list
if (!hasDuplicate) {
tmpToolInstallerEntries.add(data1);
}
}
reducedToolEntries = tmpToolInstallerEntries;
}
ToolInstallerList toolInstallerList = new ToolInstallerList();
toolInstallerList.list = new ToolInstallerEntry[reducedToolEntries.size()];
reducedToolEntries.toArray(toolInstallerList.list);
JSONObject reducedToolEntriesJsonList = JSONObject.fromObject(toolInstallerList);
//return the list with no duplicates
return reducedToolEntriesJsonList;
}
/**
* This ID needs to be unique, and needs to match the ID token in the JSON update file.
* <p>
* By default we use the fully-qualified class name of the {@link DownloadFromUrlInstaller} subtype.
*/
public String getId() {
return clazz.getName().replace('$','.');
}
/**
* List of installable tools.
*
* <p>
* The UI uses this information to populate the drop-down. Subtypes can override this method
* if it wants to change the way the list is filled.
*
* @return never null.
*/
public List<? extends Installable> getInstallables() throws IOException {
JSONObject d = Downloadable.get(getId()).getData();
if(d==null) return Collections.emptyList();
return Arrays.asList(((InstallableList)JSONObject.toBean(d,InstallableList.class)).list);
}
}
/**
* Used for JSON databinding to parse the obtained list.
*/
public static class InstallableList {
// initialize with an empty array just in case JSON doesn't have the list field (which shouldn't happen.)
public Installable[] list = new Installable[0];
}
/**
* Downloadable and installable tool.
*/
public static class Installable {
/**
* Used internally to uniquely identify the name.
*/
public String id;
/**
* This is the human readable name.
*/
public String name;
/**
* URL.
*/
public String url;
}
/**
* Convenient abstract class to implement a NodeSpecificInstallable based on an existing Installable
* @since 1.626
*/
public abstract class NodeSpecificInstallable extends Installable implements NodeSpecific<NodeSpecificInstallable> {
public NodeSpecificInstallable(Installable inst) {
this.id = inst.id;
this.name = inst.name;
this.url = inst.url;
}
}
}