/*******************************************************************************
* Copyright (c) 2013, 2015 Pivotal, Inc.
* 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:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.wizard.content;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.springsource.ide.eclipse.commons.core.util.OsUtils;
import org.springsource.ide.eclipse.commons.frameworks.core.downloadmanager.DownloadableItem;
import org.springsource.ide.eclipse.commons.frameworks.core.downloadmanager.UIThreadDownloadDisallowed;
import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil;
import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult;
/**
* A CodeSet represents a bunch of content that can somehow be imported into
* the workspace to create a project.
* <p>
* A CodeSet instance is more like a handle to content than actual content.
* I.e. it contains enough information to fetch the content but doesn't guarantee that the
* content actually exists.
* <p>
* This means it is possible to create CodeSet instances without having to pay the
* cost of downloading the Zip file upfront. Some operations on CodeSets however
* will force download to be attempted.
*
* @author Kris De Volder
*/
public abstract class CodeSet {
private static final boolean IS_WINDOWS = OsUtils.isWindows();
/**
* Represents an Entry in a CodeSet. API similar to ZipEntry but paths may be remapped
* as they are relative to the root of the code set not the Zip (or other source of data)
*/
public abstract class CodeSetEntry {
public abstract IPath getPath();
public abstract boolean isDirectory();
public abstract InputStream getData() throws IOException;
/**
* If a given implementation of CodeSetEntry has the means to determine
* permission flags, then it can override this method.
*/
public int getUnixMode() {
return 0;
}
}
public static abstract class Processor<T> {
/**
* Do some work an a ZipEntry in a CodeSet.
*/
public abstract T doit(CodeSetEntry e) throws Exception;
}
protected String name;
private List<BuildType> buildTypes;
public CodeSet(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public static CodeSet fromZip(String name, DownloadableItem zip, IPath root) {
return new ZipFileCodeSet(name, zip, root);
}
/**
* Fetch list of build types that are supported by the CodeSet. Note that
* determining this information requires inspecting some of the directory
* entries in the CodeSet and therefore it requires downloading the
* content / zip / repo the CodeSet is contained in (unless it is
* already cached locally).
*/
public List<BuildType> getBuildTypes() throws UIThreadDownloadDisallowed {
//Only use 'default' build type if no other build type works.
if (this.buildTypes==null) {
//Careful if the code in here throws (e.g. because not yet downloaded and in UI thread...
//then do NOT stick an empty list into this.buildTypes!
ArrayList<BuildType> buildTypes = new ArrayList<BuildType>();
for (BuildType type : BuildType.values()) {
IPath scriptFile = type.getBuildScript();
if (scriptFile!=null && hasFile(scriptFile)) {
buildTypes.add(type);
}
}
if (buildTypes.isEmpty()) {
buildTypes.add(BuildType.GENERAL);
}
this.buildTypes = buildTypes;
}
return this.buildTypes;
}
/**
* Returns true if content for this codeset can be downloaded and
* contains some data.
*
* If the CodeSet was not previously downloaded this will force a
* download.
*/
public abstract boolean exists() throws Exception;
/**
* Returns true if this codeset has a file with a given path.
*
* If the CodeSet was not previously downloaded this will force a
* download.
* @throws UIThreadDownloadDisallowed
*/
public abstract boolean hasFile(IPath path) throws UIThreadDownloadDisallowed;
/**
* Returns true if this codeset has a folder with a given path.
*
* If the CodeSet was not previously downloaded this will force a
* download.
*/
public abstract boolean hasFolder(IPath path);
/**
* Convenience method that allows passing paths as Strings instead of IPath instances
*/
public final boolean hasFile(String path) throws UIThreadDownloadDisallowed {
return hasFile(new Path(path));
}
/**
* Perform some work on all content element in this codeSet.
* The processor may return a non-null value or throw an Exception to
* stop processing in the middle of the iteration.
*/
public abstract <T> T each(Processor<T> processor) throws Exception;
/**
* Copies the contents of codeset to a given filesystem location
*/
public void createAt(final File location) throws Exception {
if (location.exists()) {
if (!FileUtils.deleteQuietly(location)) {
throw new IOException("Data already exists at location and it could not be deleted: "+location);
}
}
each(new CodeSet.Processor<Void>() {
@Override
public Void doit(CodeSetEntry e) throws Exception {
IPath path = e.getPath();
File target = new File(location, path.toString());
if (e.isDirectory()) {
target.mkdirs();
} else {
IOUtil.pipe(e.getData(), target);
if (!IS_WINDOWS) {
int mode = e.getUnixMode();
if (mode>0) {
Files.setPosixFilePermissions(target.toPath(), OsUtils.posixFilePermissions(mode));
}
}
}
return null;
}
});
}
public ValidationResult validateBuildType(BuildType buildType) throws UIThreadDownloadDisallowed {
List<BuildType> validBuildTypes = getBuildTypes();
if (validBuildTypes.contains(buildType)) {
return ValidationResult.OK;
}
//Not valid formulate a nice explanation
IPath expectedScript = buildType.getBuildScript();
if (expectedScript!=null) {
return ValidationResult.error("Can't use '"+buildType.displayName()+"': There is no '"+expectedScript+"'");
} else {
StringBuilder message = new StringBuilder("Should be imported as ");
boolean first = true;
for (BuildType bt : validBuildTypes) {
if (!first) {
message.append(" or ");
}
message.append("'"+bt.displayName()+"'");
first = false;
}
return ValidationResult.error(message.toString());
}
}
/**
* Read a single entry from a codeset. Note that this operation might be expensive because
* it might open and close a zipfile for a ZipFileCodeSet).
*/
public abstract <T> T readFileEntry(String path, Processor<T> processor) throws Exception;
}