/* * The MIT License * * Copyright (c) 2011-2014, CloudBees, Inc. * * 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.cloudbees.plugins.deployer.impl.run; import com.cloudbees.EndPoints; import com.cloudbees.api.BeesClient; import com.cloudbees.api.BeesClientConfiguration; import com.cloudbees.api.ServiceResourceInfo; import com.cloudbees.api.ServiceResourceListResponse; import com.cloudbees.api.ServiceSubscriptionInfo; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.cloudbees.CloudBeesAccount; import com.cloudbees.plugins.credentials.cloudbees.CloudBeesUser; import com.cloudbees.plugins.deployer.DeployNowRunAction; import com.cloudbees.plugins.deployer.NamedThreadFactory; import com.cloudbees.plugins.deployer.sources.DeploySource; import com.cloudbees.plugins.deployer.targets.DeployTarget; import com.cloudbees.plugins.deployer.targets.DeployTargetDescriptor; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.ProxyConfiguration; import hudson.RelativePath; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.Hudson; import hudson.model.Item; import hudson.model.TaskListener; import hudson.security.ACL; import hudson.util.ComboBoxModel; import hudson.util.ExceptionCatchingThreadFactory; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import hudson.util.TimeUnit2; import org.acegisecurity.Authentication; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; import javax.servlet.ServletException; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author stephenc * @since 12/12/2012 15:00 */ public class RunTargetImpl extends DeployTarget<RunTargetImpl> { private static final Logger LOGGER = Logger.getLogger(RunTargetImpl.class.getName()); /** * The application id. */ private final String applicationId; /** * The application environment. */ private final String applicationEnvironment; /** * The application configuration. */ @CheckForNull private final Setting[] applicationConfig; /** * The RUN API end-point for the server holding the application */ private final String apiEndPoint; /** * The description to use for the deployment. */ private final String deploymentDescription; private final boolean deltaDeployment; private final String clickStackName; @CheckForNull private final Setting[] clickStackConfig; @CheckForNull private final Setting[] clickStackRuntimeConfig; /** * @deprecated Retained for backwards API compatibility */ @Deprecated public RunTargetImpl(String apiEndPoint, String applicationId, String applicationEnvironment, String deploymentDescription, Setting[] applicationConfig, DeploySource artifact) { this(apiEndPoint, applicationId, applicationEnvironment, deploymentDescription, applicationConfig, artifact, false, null, null, null); } /** * @since 4.14 */ @DataBoundConstructor public RunTargetImpl(String apiEndPoint, String applicationId, String applicationEnvironment, String deploymentDescription, Setting[] applicationConfig, DeploySource artifact, boolean deltaDeployment, String clickStackName, Setting[] clickStackConfig, Setting[] clickStackRuntimeConfig) { super(artifact); this.apiEndPoint = StringUtils.isBlank(apiEndPoint) ? EndPoints.runAPI() : apiEndPoint; this.applicationConfig = applicationConfig == null ? new Setting[0] : applicationConfig.clone(); this.applicationEnvironment = Util.fixEmptyAndTrim(applicationEnvironment); this.applicationId = Util.fixEmptyAndTrim(applicationId); this.deploymentDescription = Util.fixEmptyAndTrim(deploymentDescription); this.deltaDeployment = deltaDeployment; this.clickStackName = Util.fixEmptyAndTrim(clickStackName); this.clickStackConfig = clickStackConfig == null ? new Setting[0] : clickStackConfig.clone(); this.clickStackRuntimeConfig = clickStackRuntimeConfig == null ? new Setting[0] : clickStackRuntimeConfig.clone(); } public String getApiEndPoint() { return apiEndPoint; } public Setting[] getApplicationConfig() { return applicationConfig; } public String getApplicationEnvironment() { return applicationEnvironment; } /** * Gets the application environment that the application should be deployed as for a specific context. * * @param context the context. * @param listener the listener. * @return the application environment to deploy the application as never {@code null}. * @throws MacroEvaluationException if macros could not be evaluated. * @throws IOException if an IO exception occured. * @throws InterruptedException if interrupted. */ public String getApplicationEnvironment(AbstractBuild<?, ?> context, TaskListener listener) throws MacroEvaluationException, IOException, InterruptedException { if (StringUtils.isEmpty(getApplicationEnvironment())) { return "run"; } else { String result = expandAllMacros(context, listener, getApplicationEnvironment()); return StringUtils.isEmpty(result) ? "run" : result; } } @NonNull public String getDeploymentDescription() { return deploymentDescription == null ? "${JOB_NAME} #${BUILD_NUMBER}" : deploymentDescription; } /** * Gets the description that the application should be deployed with for a specific context. * * @param context the context. * @param listener the listener. * @return the description to deploy the application with never {@code null}. * @throws MacroEvaluationException if macros could not be evaluated. * @throws IOException if an IO exception occured. * @throws InterruptedException if interrupted. */ public String getDeploymentDescription(AbstractBuild<?, ?> context, TaskListener listener) throws MacroEvaluationException, IOException, InterruptedException { if (StringUtils.isEmpty(getDeploymentDescription())) { return context.getFullDisplayName(); } else { return expandAllMacros(context, listener, getDeploymentDescription()); } } public Setting[] getClickStackConfig() { return clickStackConfig; } public String getClickStackName() { return clickStackName; } public Setting[] getClickStackRuntimeConfig() { return clickStackRuntimeConfig; } public boolean isDeltaDeployment() { return deltaDeployment; } public String getApplicationId() { return applicationId; } public Map<String, String> getApplicationConfigMap() { Map<String, String> result = new HashMap<String, String>(); if (applicationConfig != null) { for (Setting s : applicationConfig) { final String key = Util.fixNull(s.getKey()); final String value = Util.fixNull(s.getValue()); if (StringUtils.isNotEmpty(key)) { result.put(key, value); } } } return result; } public Map<String, String> getApplicationConfigMap(AbstractBuild<?, ?> context, TaskListener listener) throws MacroEvaluationException, IOException, InterruptedException { Map<String, String> result = new HashMap<String, String>(); if (applicationConfig != null) { for (Setting s : applicationConfig) { final String key = expandAllMacros(context, listener, Util.fixNull(s.getKey())); final String value = expandAllMacros(context, listener, Util.fixNull(s.getValue())); if (StringUtils.isNotEmpty(key)) { result.put(key, value); } } } return result; } public Map<String, String> getClickStackConfigMap() { Map<String, String> result = new HashMap<String, String>(); if (clickStackConfig != null) { for (Setting s : clickStackConfig) { final String key = Util.fixNull(s.getKey()); final String value = Util.fixNull(s.getValue()); if (StringUtils.isNotEmpty(key)) { result.put(key, value); } } } return result; } public Map<String, String> getClickStackConfigMap(AbstractBuild<?, ?> context, TaskListener listener) throws MacroEvaluationException, IOException, InterruptedException { Map<String, String> result = new HashMap<String, String>(); if (clickStackConfig != null) { for (Setting s : clickStackConfig) { final String key = expandAllMacros(context, listener, Util.fixNull(s.getKey())); final String value = expandAllMacros(context, listener, Util.fixNull(s.getValue())); if (StringUtils.isNotEmpty(key)) { result.put(key, value); } } } return result; } public Map<String, String> getClickstackConfigMap() { Map<String, String> result = new HashMap<String, String>(); if (clickStackRuntimeConfig != null) { for (Setting s : clickStackRuntimeConfig) { final String key = Util.fixNull(s.getKey()); final String value = Util.fixNull(s.getValue()); if (StringUtils.isNotEmpty(key)) { result.put(key, value); } } } return result; } public Map<String, String> getClickStackRuntimeConfigMap(AbstractBuild<?, ?> context, TaskListener listener) throws MacroEvaluationException, IOException, InterruptedException { Map<String, String> result = new HashMap<String, String>(); if (clickStackRuntimeConfig != null) { for (Setting s : clickStackRuntimeConfig) { final String key = expandAllMacros(context, listener, Util.fixNull(s.getKey())); final String value = expandAllMacros(context, listener, Util.fixNull(s.getValue())); if (StringUtils.isNotEmpty(key)) { result.put(key, value); } } } return result; } public String getApplicationId(AbstractBuild<?, ?> context, BuildListener listener) throws MacroEvaluationException, IOException, InterruptedException { return expandAllMacros(context, listener, applicationId); } public String getClickStackName(AbstractBuild<?, ?> context, BuildListener listener) throws MacroEvaluationException, IOException, InterruptedException { return expandAllMacros(context, listener, clickStackName); } @Override public String getDisplayName() { return getApplicationId(); } @Override protected boolean isArtifactFileValid(File file) { return file.isFile(); } @Override protected boolean isComplete() { return StringUtils.isNotBlank(applicationId); } @Override public String toString() { final StringBuilder sb = new StringBuilder("RunTargetImpl{"); sb.append("artifact=").append(getArtifact()); sb.append(", apiEndPoint='").append(apiEndPoint).append('\''); sb.append(", applicationId='").append(applicationId).append('\''); sb.append(", applicationEnvironment='").append(applicationEnvironment).append('\''); sb.append(", applicationConfig=").append(Arrays.toString(applicationConfig)); sb.append(", deploymentDescription='").append(deploymentDescription).append('\''); sb.append(", deltaDeployment=").append(deltaDeployment); sb.append(", clickStackName='").append(clickStackName).append('\''); sb.append(", clickStackConfig=").append(Arrays.toString(clickStackConfig)); sb.append(", clickStackRuntimeConfig=").append(Arrays.toString(clickStackRuntimeConfig)); sb.append('}'); return sb.toString(); } @Extension public static class DescriptorImpl extends DeployTargetDescriptor<RunTargetImpl> { public final ExecutorService executorService = new ThreadPoolExecutor(0, 2, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new NamedThreadFactory("RunTargetImpl:UI-Queries", new ExceptionCatchingThreadFactory( Executors.defaultThreadFactory()))); private static class CachedMap<K, V> { private final long expires; private final Future<Map<K, V>> statuses; private CachedMap(Future<Map<K, V>> statuses) { this.expires = System.currentTimeMillis() + TimeUnit2.MINUTES.toMillis(1); this.statuses = statuses; } public boolean isExpired() { return expires < System.currentTimeMillis(); } public Future<Map<K, V>> getStatuses() { return statuses; } } private final Map<String, CachedMap<String, String>> applicationStatusCache = new LinkedHashMap<String, CachedMap<String, String>>(); private final Map<String, CachedMap<String, String>> accountEndpointsCache = new LinkedHashMap<String, CachedMap<String, String>>(); @Override public String getDisplayName() { return Messages.CloudBeesRunTarget_DisplayName(); } @NonNull private Map<String, String> getApplicationStatuses(CloudBeesUser cloudBeesUser, CloudBeesAccount cloudBeesAccount) throws IOException, InterruptedException, ExecutionException, TimeoutException { final String cacheKey = cloudBeesUser.getName() + ":" + cloudBeesAccount.getName(); Map<String, String> applicationStatuses; CachedMap<String, String> cacheValue; synchronized (applicationStatusCache) { cacheValue = applicationStatusCache.get(cacheKey); if (cacheValue != null && cacheValue.isExpired()) { applicationStatusCache.remove(cacheKey); cacheValue = null; } } if (cacheValue != null) { applicationStatuses = cacheValue.getStatuses().get(30, TimeUnit.SECONDS); if (applicationStatuses != null) { return applicationStatuses; } return Collections.emptyMap(); } BeesClientConfiguration config = new BeesClientConfiguration(EndPoints.runAPI(), cloudBeesUser.getAPIKey(), cloudBeesUser.getAPISecret().getPlainText(), "xml", "1.0"); if (Hudson.getInstance() != null && Hudson.getInstance().proxy != null) { final ProxyConfiguration proxy = Hudson.getInstance().proxy; config.setProxyHost(proxy.name); config.setProxyPort(proxy.port); config.setProxyUser(proxy.getUserName()); config.setProxyPassword(proxy.getPassword()); } final BeesClient client = new BeesClient(config); cacheValue = new CachedMap<String, String>(executorService.submit( new AccountRegionsCallable(client, cloudBeesAccount))); synchronized (applicationStatusCache) { applicationStatusCache.put(cacheKey, cacheValue); } applicationStatuses = cacheValue.getStatuses().get(30, TimeUnit.SECONDS); if (applicationStatuses != null) { return applicationStatuses; } return Collections.emptyMap(); } private Map<String, String> getAccountEndpoints(CloudBeesUser cloudBeesUser, CloudBeesAccount cloudBeesAccount) throws IOException, InterruptedException, ExecutionException, TimeoutException { final String cacheKey = cloudBeesUser.getName() + ":" + cloudBeesAccount.getName(); Map<String, String> accountEndpoints; CachedMap<String, String> cacheValue; synchronized (accountEndpointsCache) { cacheValue = accountEndpointsCache.get(cacheKey); if (cacheValue != null && cacheValue.isExpired()) { accountEndpointsCache.remove(cacheKey); cacheValue = null; } } if (cacheValue != null) { accountEndpoints = cacheValue.getStatuses().get(30, TimeUnit.SECONDS); if (accountEndpoints != null) { return accountEndpoints; } return Collections.emptyMap(); } BeesClientConfiguration config = new BeesClientConfiguration(EndPoints.runAPI(), cloudBeesUser.getAPIKey(), cloudBeesUser.getAPISecret().getPlainText(), "xml", "1.0"); if (Hudson.getInstance() != null && Hudson.getInstance().proxy != null) { final ProxyConfiguration proxy = Hudson.getInstance().proxy; config.setProxyHost(proxy.name); config.setProxyPort(proxy.port); config.setProxyUser(proxy.getUserName()); config.setProxyPassword(proxy.getPassword()); } final BeesClient client = new BeesClient(config); cacheValue = new CachedMap<String, String>(executorService.submit( new AccountEndpointsCallable(client, cloudBeesAccount))); synchronized (accountEndpointsCache) { accountEndpointsCache.put(cacheKey, cacheValue); } accountEndpoints = cacheValue.getStatuses().get(30, TimeUnit.SECONDS); if (accountEndpoints != null) { return accountEndpoints; } return Collections.emptyMap(); } @SuppressWarnings("unused") // used by stapler public FormValidation doCheckApplicationId(@QueryParameter @RelativePath("..") String usersAuth, @QueryParameter final String value, @QueryParameter @RelativePath("..") final String user, @QueryParameter @RelativePath("..") final String account) throws IOException, ServletException { try { if (StringUtils.isBlank(user) || StringUtils.isBlank(account)) { return FormValidation.ok(); // somebody else will flag this issue } if (StringUtils.isBlank(value)) { return FormValidation.error("Application Id cannot be empty"); } Item item = Stapler.getCurrentRequest().findAncestorObject(Item.class); CloudBeesUser cloudBeesUser = null; if (!StringUtils.isEmpty(usersAuth) && item.hasPermission(DeployNowRunAction.OWN_AUTH)) { cloudBeesUser = getCloudBeesUser(user, Hudson.getAuthentication()); } if (cloudBeesUser == null && item.hasPermission(DeployNowRunAction.JOB_AUTH)) { cloudBeesUser = getCloudBeesUser(user, ACL.SYSTEM); } if (cloudBeesUser == null) { return FormValidation.ok(); // somebody else will flag this issue } CloudBeesAccount cloudBeesAccount = cloudBeesUser.getAccount(account); if (cloudBeesAccount == null) { return FormValidation.ok(); // somebody else will flag this issue } final Map<String, String> statuses = getApplicationStatuses(cloudBeesUser, cloudBeesAccount); if (statuses.containsKey(value) || statuses.containsKey(account + "/" + value)) { return FormValidation.ok(); } return FormValidation .warning("This application ID was not found, so using it will create a new application"); } catch (Exception e) { return FormValidation.error(e, "Error during check of Application Id: " + e.getMessage()); } } @SuppressWarnings("unused") // used by stapler public ComboBoxModel doFillApplicationIdItems(@QueryParameter @RelativePath("..") final String usersAuth, @QueryParameter @RelativePath("..") final String user, @QueryParameter @RelativePath("..") final String account) { try { if (StringUtils.isBlank(user) || StringUtils.isBlank(account)) { return new ComboBoxModel(); } Item item = Stapler.getCurrentRequest().findAncestorObject(Item.class); CloudBeesUser cloudBeesUser = null; if (!StringUtils.isEmpty(usersAuth) && item.hasPermission(DeployNowRunAction.OWN_AUTH)) { cloudBeesUser = getCloudBeesUser(user, Hudson.getAuthentication()); } if (cloudBeesUser == null && item.hasPermission(DeployNowRunAction.JOB_AUTH)) { cloudBeesUser = getCloudBeesUser(user, ACL.SYSTEM); } if (cloudBeesUser == null) { return new ComboBoxModel(); } CloudBeesAccount cloudBeesAccount = cloudBeesUser.getAccount(account); if (cloudBeesAccount == null) { return new ComboBoxModel(); } Set<String> names = new TreeSet<String>(); for (String name : getApplicationStatuses(cloudBeesUser, cloudBeesAccount).keySet()) { names.add(name); } return new ComboBoxModel(names); } catch (Exception e) { return new ComboBoxModel(); } } public ListBoxModel doFillApiEndPointItems(@QueryParameter @RelativePath("..") final String usersAuth, @QueryParameter @RelativePath("..") final String user, @QueryParameter @RelativePath("..") final String account, @QueryParameter final String applicationId) { Map<String, String> values = new LinkedHashMap<String, String>(); String appIdRegion = null; values.put(EndPoints.runAPI(), "US"); try { if (!StringUtils.isBlank(user) && !StringUtils.isBlank(account)) { Item item = Stapler.getCurrentRequest().findAncestorObject(Item.class); CloudBeesUser cloudBeesUser = null; if (!StringUtils.isEmpty(usersAuth) && item.hasPermission(DeployNowRunAction.OWN_AUTH)) { cloudBeesUser = getCloudBeesUser(user, Hudson.getAuthentication()); } if (cloudBeesUser == null && item.hasPermission(DeployNowRunAction.JOB_AUTH)) { cloudBeesUser = getCloudBeesUser(user, ACL.SYSTEM); } if (cloudBeesUser != null) { CloudBeesAccount cloudBeesAccount = cloudBeesUser.getAccount(account); if (cloudBeesAccount != null) { BeesClientConfiguration config = new BeesClientConfiguration(EndPoints.runAPI(), cloudBeesUser.getAPIKey(), cloudBeesUser.getAPISecret().getPlainText(), "xml", "1.0"); if (Hudson.getInstance() != null && Hudson.getInstance().proxy != null) { final ProxyConfiguration proxy = Hudson.getInstance().proxy; config.setProxyHost(proxy.name); config.setProxyPort(proxy.port); config.setProxyUser(proxy.getUserName()); config.setProxyPassword(proxy.getPassword()); } BeesClient client = new BeesClient(config); if (!StringUtils.isBlank(applicationId)) { try { appIdRegion = getApplicationStatuses(cloudBeesUser, cloudBeesAccount).get(applicationId); } catch (Exception e) { // ignore } } values.putAll(getAccountEndpoints(cloudBeesUser, cloudBeesAccount)); } } } } catch (Exception e) { LOGGER.log(Level.INFO, "Could not populate ListBoxModel", e); } ListBoxModel result = new ListBoxModel(); for (Map.Entry<String, String> entry : values.entrySet()) { result.add(new ListBoxModel.Option(entry.getValue(), entry.getKey(), StringUtils.equalsIgnoreCase(entry.getValue(), appIdRegion))); } return result; } private static CloudBeesUser getCloudBeesUser(String user, Authentication authentication) { for (CloudBeesUser u : CredentialsProvider.lookupCredentials(CloudBeesUser.class, Stapler.getCurrentRequest().findAncestorObject(Item.class), authentication)) { if (u.getName().equals(user)) { return u; } } return null; } public FormValidation doCheckApplicationParameterName(@QueryParameter String value) { if (StringUtils.isBlank(value)) { return FormValidation.error("Must not be empty"); } return FormValidation.ok(); } private static class AccountRegionsCallable implements Callable<Map<String, String>> { private final String account; private final BeesClient client; private final CloudBeesAccount cloudBeesAccount; public AccountRegionsCallable(BeesClient client, CloudBeesAccount cloudBeesAccount) { this.client = client; this.cloudBeesAccount = cloudBeesAccount; account = cloudBeesAccount.getName(); } public Map<String, String> call() throws Exception { try { ServiceResourceListResponse response = client.serviceResourceList("cb-app", account, "application"); Map<String, String> accountRegions = new TreeMap<String, String>(); String prefix = account + "/"; for (ServiceResourceInfo resourceInfo : response.getResources()) { String id = resourceInfo.getId(); if (id.startsWith(prefix)) { String region = resourceInfo.getConfig() != null ? resourceInfo.getConfig().get("region") : null; accountRegions .put(id.substring(prefix.length()), region == null ? "US" : region.toUpperCase()); } } return accountRegions; } catch (Exception e) { LOGGER.log(Level.INFO, "Could not get list of applications: ", e); } return Collections.emptyMap(); } } private static class AccountEndpointsCallable implements Callable<Map<String, String>> { private final BeesClient client; private final CloudBeesAccount cloudBeesAccount; public AccountEndpointsCallable(BeesClient client, CloudBeesAccount cloudBeesAccount) { this.client = client; this.cloudBeesAccount = cloudBeesAccount; } public Map<String, String> call() throws Exception { Map<String, String> result = new LinkedHashMap<String, String>(); result.put(EndPoints.runAPI(), "US"); Pattern dcPattern = Pattern.compile("\\Qdc.\\E([^.]+)"); try { ServiceSubscriptionInfo subscriptionInfo = client.serviceSubscriptionInfo("cb-app", cloudBeesAccount.getName()); for (Map.Entry<String, String> entry : subscriptionInfo.getSettings().entrySet()) { Matcher matcher = dcPattern.matcher(entry.getKey()); if (matcher.matches()) { String region = matcher.group(1); if ("enabled".equalsIgnoreCase(entry.getValue()) || Boolean .parseBoolean(entry.getValue())) { String url = subscriptionInfo.getSettings().get("dc." + region + ".api.url"); if (url != null) { result.put(url, region.toUpperCase()); } } } } } catch (Exception e) { LOGGER.log(Level.INFO, "Could not get list of applications: ", e); } return result; } } } public static class Setting implements Serializable { private static final long serialVersionUID = 1L; private final String key; private final String value; @DataBoundConstructor public Setting(String key, String value) { this.key = key; this.value = value; } public String getKey() { return key; } public String getValue() { return value; } } }