/*
* The MIT License
*
* Copyright (c) 2009-2010, Manufacture Française des Pneumatiques Michelin, Romain Seguy
*
* 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 com.michelin.cio.hudson.plugins.wasbuilder;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Launcher;
import hudson.Util;
import hudson.model.EnvironmentSpecific;
import hudson.model.Hudson;
import hudson.model.Node;
import hudson.model.TaskListener;
import hudson.remoting.Callable;
import hudson.slaves.NodeSpecific;
import hudson.tools.ToolDescriptor;
import hudson.tools.ToolInstallation;
import hudson.util.FormValidation;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.jvnet.localizer.ResourceBundleHolder;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
/**
* Corresponds to an IBM WebSphere Application Server installation (currently,
* it has been tested with WAS 6.0 and WAS 7.0) or an Administration Thin Client
* (cf. {@link http://publib.boulder.ibm.com/infocenter/wasinfo/v7r0/topic/com.ibm.websphere.nd.multiplatform.doc/info/ae/ae/txml_adminclient.html).
*
* <p>To use a {@link WASBuildStep} build step, it is mandatory to define an
* installation: No default installations can be assumed as we necessarily need
* {@code wsadmin.bat}/{@code wsadmin.sh}.</p>
*
* @author Romain Seguy (http://openromain.blogspot.com)
*/
public class WASInstallation extends ToolInstallation implements NodeSpecific<WASInstallation>, EnvironmentSpecific<WASInstallation> {
public final static String WSADMIN_BAT = "wsadmin.bat";
public final static String WSADMIN_SH = "wsadmin.sh";
/**
* Represents the wsadmin command to actually invoke (most of the time, this
* has to be something like $WAS_HOME/bin/wsadmin.sh).
*/
private final String wsadminCommand;
@DataBoundConstructor
public WASInstallation(String name, String home, String wsadminCommand) {
super(name, removeTrailingBackslash(home), Collections.EMPTY_LIST);
if(StringUtils.isBlank(wsadminCommand)) {
this.wsadminCommand = "${WSADMIN}";
}
else {
this.wsadminCommand = wsadminCommand;
}
}
public WASInstallation forEnvironment(EnvVars env) {
return new WASInstallation(getName(), env.expand(getHome()), getWsadminCommand());
}
public WASInstallation forNode(Node node, TaskListener log) throws IOException, InterruptedException {
return new WASInstallation(getName(), translateFor(node, log), getWsadminCommand());
}
public static WASInstallation getWasInstallationByName(String installationName) {
for(WASInstallation installation: Hudson.getInstance().getDescriptorByType(WASInstallation.DescriptorImpl.class).getInstallations()) {
if(installationName != null && installation.getName().equals(installationName)) {
return installation;
}
}
return null;
}
public String getWsadminCommand() {
if(StringUtils.isBlank(wsadminCommand)) {
return "${WSADMIN}";
}
return wsadminCommand;
}
public String getWsadminExecutable(Launcher launcher) throws IOException, InterruptedException {
return launcher.getChannel().call(new Callable<String,IOException>() {
public String call() throws IOException {
String wsadminFilePath = null;
// 1st try: do we work with a plain WAS installation?
File wsadminFile = getWsadminFile("bin");
if(wsadminFile.exists()) {
wsadminFilePath = wsadminFile.getPath();
}
else {
// 2nd try: do we work with an administration thin client?
wsadminFile = getWsadminFile(null);
if(wsadminFile.exists()) {
wsadminFilePath = wsadminFile.getPath();
}
else {
return null;
}
}
return getWsadminCommand().replace("${WSADMIN}", wsadminFilePath);
}
});
}
/**
* Returns a {@link File} representing {@code wsadmin.bat}/{@code wsadmin.sh}.
*/
private File getWsadminFile(String binFolder) {
String wsadminFileName = WSADMIN_SH;
if(!StringUtils.isEmpty(binFolder)) {
binFolder = binFolder + "/";
}
else {
binFolder = "";
}
if(Hudson.isWindows()) {
wsadminFileName = WSADMIN_BAT;
}
return new File(Util.replaceMacro(getHome(), EnvVars.masterEnvVars), binFolder + wsadminFileName);
}
/**
* Removes the '\' or '/' character that may be present at the end of the
* specified string.
*/
private static String removeTrailingBackslash(String s) {
return StringUtils.removeEnd(StringUtils.removeEnd(s, "/"), "\\");
}
@Extension
public static class DescriptorImpl extends ToolDescriptor<WASInstallation> {
private List<WASServer> servers;
private boolean createLocks = true;
public DescriptorImpl() {
// let's avoid a NullPointerException in getInstallations()
setInstallations(new WASInstallation[0]);
load();
}
/**
* Returns the possible connection types to WAS.
*
* <p>This method needs to be placed here so that the list can be
* accessible from WASInstallation's global.jelly file: global.jelly
* is not able to access such a method if it is placed, even statically,
* into WASServer.</p>
*/
public String[] getConntypes() {
return WASServer.CONNTYPES;
}
public boolean getCreateLocks() {
return createLocks;
}
public void setCreateLocks(boolean createLocks) {
this.createLocks = createLocks;
}
@Override
public String getDisplayName() {
return ResourceBundleHolder.get(WASBuildStep.class).format("DisplayName");
}
public WASServer[] getServers() {
if(servers != null) {
return servers.toArray(new WASServer[0]);
}
return null;
}
private void setServers(WASServer... servers) {
if(servers != null) {
if(this.servers != null) {
this.servers.clear();
}
else {
this.servers = new ArrayList<WASServer>();
}
// only servers which have a name are added to the list, other
// ones are dropped
for(WASServer server: servers) {
if(StringUtils.isNotEmpty(server.getName())) {
this.servers.add(server);
}
}
// we sort the servers list, otherwise it will become a pain to
// know what's already created and what has to be created
Collections.sort(this.servers, new Comparator<WASServer>() {
public int compare(WASServer server1, WASServer server2) {
return server1.getName().compareToIgnoreCase(server2.getName());
}
});
}
}
@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
setInstallations(
req.bindJSONToList(
WASInstallation.class,
formData.get("wasinstall")).toArray(new WASInstallation[0]));
setServers(
req.bindJSONToList(
WASServer.class,
formData.get("wasserver")).toArray(new WASServer[0]));
setCreateLocks(formData.getBoolean("createLocks"));
save();
if(getCreateLocks()) {
createLocks();
}
return true;
}
/**
* Creates, for each defined WAS server, a corresponding lock (from the
* locks-and-latches plug-in).
*
* <p>All the processing here is done using the reflection API: This is
* purposely done to avoid having a dependency between this plug-in and
* the locks-and-latches one which would make it mandatory to install it.
* </p>
*/
private void createLocks() {
// is the locks-and-latches plugin installed?
if(Hudson.getInstance().getPlugin("locks-and-latches") != null) {
try {
ClassLoader hudsonClassLoader = Hudson.getInstance().getPluginManager().uberClassLoader;
// LockWrapper.DescriptorImpl lockWrapperDescriptor = LockWrapper.getDescriptor();
Class lockWrapperClass = hudsonClassLoader.loadClass("hudson.plugins.locksandlatches.LockWrapper");
Field lockWrapperDescriptorField = lockWrapperClass.getDeclaredField("DESCRIPTOR");
Object lockWrapperDescriptor = lockWrapperDescriptorField.get(null);
// String[] lockNames = lockWrapperDescriptor.getLockNames();
Class descriptorImplClass = hudsonClassLoader.loadClass("hudson.plugins.locksandlatches.LockWrapper$DescriptorImpl");
Method getLockNamesMethod = descriptorImplClass.getMethod("getLockNames");
String[] lockNames = (String[]) getLockNamesMethod.invoke(lockWrapperDescriptor);
// we ensure each server has a lock with the same name
List<WASServer> createLockFor = new ArrayList<WASServer>();
if(getServers() != null) {
for(WASServer server: getServers()) {
if(lockNames != null) {
boolean found = false;
for(String lockName: lockNames) {
if(lockName.equals(server.getName())) {
found = true;
break;
}
}
if(!found) {
createLockFor.add(server);
}
}
else {
createLockFor.add(server);
}
}
}
// we create the new locks if required
if(!createLockFor.isEmpty()) {
// new LockWrapper.LockConfig(...)
Class lockConfigClass = hudsonClassLoader.loadClass("hudson.plugins.locksandlatches.LockWrapper$LockConfig");
Constructor lockConfigConstructor = lockConfigClass.getConstructor(String.class);
List newLocks = new ArrayList();
for(WASServer server: createLockFor) {
// newLocks.add(new LockWrapper.LockConfig(server.getName()));
newLocks.add(lockConfigConstructor.newInstance(server.getName()));
LOGGER.info("The locks-and-latches plugin is installed: Adding new lock for WAS server " + server.getName());
}
// lockWrapperDescriptor.getLocks().addAll(newLocks);
Method getLocksMethod = descriptorImplClass.getMethod("getLocks");
((List) getLocksMethod.invoke(lockWrapperDescriptor)).addAll(newLocks);
// lockWrapperDescriptor.save();
Method saveMethod = descriptorImplClass.getMethod("save");
saveMethod.invoke(lockWrapperDescriptor);
}
} catch (Exception e) {
LOGGER.warning("Can't automatically add locks for WAS servers; The following exception occurred while reflecting the locks-and-latches plugin: " + e);
}
}
else {
LOGGER.warning("The locks-and-latches plugin is not installed: Can't automatically add locks for each WAS server.");
}
}
/**
* Checks if the installation folder is valid.
*/
public FormValidation doCheckHome(@QueryParameter File value) {
if(value == null || value.getPath().length() == 0) {
return FormValidation.error(ResourceBundleHolder.get(WASInstallation.class).format("InstallationFolderMustBeSet"));
}
if(!value.isDirectory()) {
return FormValidation.error(ResourceBundleHolder.get(WASInstallation.class).format("NotAFolder", value));
}
// let's check for the wsadmin file existence
if(Hudson.isWindows()) {
boolean noWsadminBat = false; // plain WAS installation
boolean noWsadminThinClientBat = false; // WAS administration thin client
File wsadminFile = new File(value, "bin\\" + WSADMIN_BAT);
if(!wsadminFile.exists()) {
noWsadminBat = true;
wsadminFile = new File(value, WSADMIN_BAT);
if(!wsadminFile.exists()) {
noWsadminThinClientBat = true;
}
}
if(noWsadminThinClientBat || noWsadminThinClientBat && noWsadminBat) {
return FormValidation.error(ResourceBundleHolder.get(WASInstallation.class).format("NotAWASInstallationFolder", value));
}
}
else {
boolean noWsadminSh = false; // plain WAS installation
boolean noWsadminThinClientSh = false; // WAS administration thin client
File wsadminFile = new File(value, "bin/" + WSADMIN_SH);
if(!wsadminFile.exists()) {
noWsadminSh = true;
wsadminFile = new File(value, WSADMIN_SH);
if(!wsadminFile.exists()) {
noWsadminThinClientSh = true;
}
}
if(noWsadminThinClientSh || noWsadminThinClientSh && noWsadminSh) {
return FormValidation.error(ResourceBundleHolder.get(WASInstallation.class).format("NotAWASInstallationFolder", value));
}
}
return FormValidation.ok();
}
// --- WASServer checks ---
public FormValidation doCheckName(@QueryParameter String value) {
if(value == null || value.length() == 0) {
return FormValidation.error(ResourceBundleHolder.get(WASServer.class).format("NameMustBeSet"));
}
return FormValidation.ok();
}
public FormValidation doCheckConntype(@QueryParameter String value) {
if(value == null) {
return FormValidation.error(ResourceBundleHolder.get(WASServer.class).format("ConntypeMustBeSet"));
}
if(!Arrays.asList(WASServer.CONNTYPES).contains(value)) {
return FormValidation.error(ResourceBundleHolder.get(WASServer.class).format("InvalidConntype", value));
}
return FormValidation.ok();
}
public FormValidation doCheckHost(@QueryParameter String value) throws IOException, ServletException {
if(value == null || value.length() == 0) {
return FormValidation.error(ResourceBundleHolder.get(WASServer.class).format("HostMustBeSet"));
}
InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(value);
}
catch(UnknownHostException uhe) {
return FormValidation.error(ResourceBundleHolder.get(WASServer.class).format("HostNotValid", value));
}
catch(SecurityException se) {
return FormValidation.error(ResourceBundleHolder.get(WASServer.class).format("HostSecurityException", value));
}
try {
if(!inetAddress.isReachable(1000)) {
throw new IOException();
}
}
catch(IOException ioe) {
return FormValidation.error(ResourceBundleHolder.get(WASServer.class).format("HostCantBeReached", value));
}
return FormValidation.ok();
}
public FormValidation doCheckPort(@QueryParameter String value) {
if(value == null || value.length() == 0) {
return FormValidation.error(ResourceBundleHolder.get(WASServer.class).format("PortMustBeSet"));
}
int port;
try {
port = Integer.parseInt(value);
if(port < 0 || port > 65535) {
return FormValidation.error(ResourceBundleHolder.get(WASServer.class).format("PortMustBeInteger"));
}
}
catch(NumberFormatException nfe) {
return FormValidation.error(ResourceBundleHolder.get(WASServer.class).format("PortMustBeInteger"));
}
if(port < 1024 || port > 49151) {
return FormValidation.warning(ResourceBundleHolder.get(WASServer.class).format("PortNotPreferredValue", port));
}
return FormValidation.ok();
}
public FormValidation doCheckUser(@QueryParameter String value) {
if(value == null || value.length() == 0) {
return FormValidation.warning(ResourceBundleHolder.get(WASServer.class).format("UserMustBeSetIfSecurityEnabled"));
}
return FormValidation.ok();
}
public FormValidation doCheckPassword(@QueryParameter String value) {
if(value == null || value.length() == 0) {
return FormValidation.warning(ResourceBundleHolder.get(WASServer.class).format("PasswordMustBeSetIfSecurityEnabled"));
}
return FormValidation.ok();
}
}
private final static Logger LOGGER = Logger.getLogger(WASInstallation.class.getName());
}