From 391800ed388bc9afef7ebeea8f180b02dcbf6ae6 Mon Sep 17 00:00:00 2001 From: Christian Steiger Date: Wed, 14 Aug 2013 11:25:51 +0200 Subject: [PATCH] final version --- .gitignore | 6 + pom.xml | 101 +++ .../plugins/relution/Application.java | 214 ++++++ .../relution/ApplicationFileCallable.java | 117 +++ .../relution/GlobalConfigurationImpl.java | 363 +++++++++ .../relution/RelutionCommunicator.java | 293 +++++++ .../plugins/relution/RelutionPublisher.java | 189 +++++ .../entities/ApplicationInformation.java | 25 + .../relution/entities/ApplicationList.java | 36 + .../entities/ShortApplicationInformation.java | 21 + .../plugins/relution/json/APIObject.java | 723 ++++++++++++++++++ .../plugins/relution/net/Request.java | 136 ++++ .../plugins/relution/net/RequestFactory.java | 232 ++++++ .../relution/net/RequestQueryFields.java | 124 +++ src/main/resources/index.jelly | 6 + .../GlobalConfigurationImpl/config.jelly | 83 ++ .../plugins/relution/Messages.properties | 8 + .../plugins/relution/Messages_de.properties | 8 + .../relution/RelutionPublisher/config.jelly | 72 ++ src/main/webapp/help-apiEndpoint.html | 3 + src/main/webapp/help-apiEndpointURL.html | 3 + src/main/webapp/help-apiOrganization.html | 3 + src/main/webapp/help-apiPassword.html | 3 + src/main/webapp/help-apiReleaseStatus.html | 3 + src/main/webapp/help-apiUsername.html | 3 + src/main/webapp/help-applicationFile.html | 5 + src/main/webapp/help-applicationIcon.html | 4 + src/main/webapp/help-applicationName.html | 3 + .../webapp/help-applicationReleaseNotes.html | 3 + 29 files changed, 2790 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/org/jenkinsci/plugins/relution/Application.java create mode 100644 src/main/java/org/jenkinsci/plugins/relution/ApplicationFileCallable.java create mode 100644 src/main/java/org/jenkinsci/plugins/relution/GlobalConfigurationImpl.java create mode 100644 src/main/java/org/jenkinsci/plugins/relution/RelutionCommunicator.java create mode 100644 src/main/java/org/jenkinsci/plugins/relution/RelutionPublisher.java create mode 100644 src/main/java/org/jenkinsci/plugins/relution/entities/ApplicationInformation.java create mode 100644 src/main/java/org/jenkinsci/plugins/relution/entities/ApplicationList.java create mode 100644 src/main/java/org/jenkinsci/plugins/relution/entities/ShortApplicationInformation.java create mode 100644 src/main/java/org/jenkinsci/plugins/relution/json/APIObject.java create mode 100644 src/main/java/org/jenkinsci/plugins/relution/net/Request.java create mode 100644 src/main/java/org/jenkinsci/plugins/relution/net/RequestFactory.java create mode 100644 src/main/java/org/jenkinsci/plugins/relution/net/RequestQueryFields.java create mode 100644 src/main/resources/index.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/relution/GlobalConfigurationImpl/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/relution/Messages.properties create mode 100644 src/main/resources/org/jenkinsci/plugins/relution/Messages_de.properties create mode 100644 src/main/resources/org/jenkinsci/plugins/relution/RelutionPublisher/config.jelly create mode 100644 src/main/webapp/help-apiEndpoint.html create mode 100644 src/main/webapp/help-apiEndpointURL.html create mode 100644 src/main/webapp/help-apiOrganization.html create mode 100644 src/main/webapp/help-apiPassword.html create mode 100644 src/main/webapp/help-apiReleaseStatus.html create mode 100644 src/main/webapp/help-apiUsername.html create mode 100644 src/main/webapp/help-applicationFile.html create mode 100644 src/main/webapp/help-applicationIcon.html create mode 100644 src/main/webapp/help-applicationName.html create mode 100644 src/main/webapp/help-applicationReleaseNotes.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5d7212 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.settings +.classpath +.project +target/ + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..41a02b8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,101 @@ + + 4.0.0 + + org.jenkins-ci.plugins + plugin + 1.526 + + + org.jenkins-ci.plugins + Relution + 2.0-SNAPSHOT + hpi + + + + + repo.jenkins-ci.org + http://repo.jenkins-ci.org/public/ + + + + + + repo.jenkins-ci.org + http://repo.jenkins-ci.org/public/ + + + + + + + commons-validator + commons-validator + 1.4.0 + + + + + commons-codec + commons-codec + 1.8 + + + + org.apache.httpcomponents + httpcore + 4.2.4 + + + + org.apache.httpcomponents + httpclient + 4.2.5 + + + + org.apache.httpcomponents + httpmime + 4.2.5 + + + + + com.google.code.gson + gson + 2.2.4 + + + + com.google.code.typica + typica + 1.7.2 + + + + + + 1.97 + + diff --git a/src/main/java/org/jenkinsci/plugins/relution/Application.java b/src/main/java/org/jenkinsci/plugins/relution/Application.java new file mode 100644 index 0000000..ede5c8f --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/relution/Application.java @@ -0,0 +1,214 @@ + +package org.jenkinsci.plugins.relution; + +import hudson.Extension; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; + +import java.io.IOException; +import java.util.Map; + +import javax.inject.Inject; +import javax.servlet.ServletException; + +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + + +public class Application extends AbstractDescribableImpl { + + private String applicationFile; + private String applicationIcon; + private String apiReleaseStatus; + private String apiEndpointURL; + private String applicationName; + private String applicationReleaseNotes; + + /** + * These constructor will be executed every time when the save/submit button will be triggered in the Jenkins. + * @param apiEndpointURL URL to which the app should communicate. + * @param applicationFile String representation of the File which should be uploaded. + * @param applicationIcon String Representation of the Icon which should be used for the uploaded app. + * @param apiReleaseStatus String Representation of the ReleaseState to the app will be published. + */ + @DataBoundConstructor + public Application(final String apiEndpointURL, final String applicationFile, final String applicationIcon, final String apiReleaseStatus, final String applicationName, final String applicationReleaseNotes) { + this.setApiEndpointURL(apiEndpointURL); + this.setApplicationFile(applicationFile); + this.setApplicationIcon(applicationIcon); + this.setApiReleaseStatus(apiReleaseStatus); + this.setApplicationName(applicationName); + this.setApplicationReleaseNotes(applicationReleaseNotes); + } + + /** + * @return URL to which your app will connect. + */ + public String getApiEndpointURL() { + return this.apiEndpointURL; + } + + /** + * @param apiEndpointURL Communication endpoint to set. + */ + public void setApiEndpointURL(final String apiEndpointURL) { + this.apiEndpointURL = apiEndpointURL; + } + + /** + * @return File which will be uploaded to relution. + */ + public String getApplicationFile() { + return this.applicationFile; + } + + /** + * @param applicationFile File that should be uploaded to relution. + */ + public void setApplicationFile(final String applicationFile) { + this.applicationFile = applicationFile; + } + + /** + * @return Pictures that will be uploaded to relution and represents your app. + */ + public String getApplicationIcon() { + return this.applicationIcon; + } + + /** + * @param applicationIcon Picture that should represent your app in relution. + */ + public void setApplicationIcon(final String applicationIcon) { + this.applicationIcon = applicationIcon; + } + + /** + * @return Publsih your app with the current releasestate. + */ + public String getApiReleaseStatus() { + return this.apiReleaseStatus; + } + + /** + * @param apiReleaseStatus ReleaseState that your app will have after upload to relution. + */ + public void setApiReleaseStatus(final String apiReleaseStatus) { + this.apiReleaseStatus = apiReleaseStatus; + } + + /** + * The Name of these app corresponds to the file which will be uploaded. + * If the textfield in the Job-Configuration is not null your app will be represented by these name + * @return The Name of the app in relution store. + */ + public String getApplicationName() { + return this.applicationName; + } + + /** + * @param applicationName Name of the new uploaded app + */ + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + + /** + * @return Content of the Log-File which will be set in the TextField. + */ + public String getApplicationReleaseNotes() { + return this.applicationReleaseNotes; + } + + /** + * @param applicationReleaseNotes Content that should be set to the app. + */ + public void setApplicationReleaseNotes(String applicationReleaseNotes) { + this.applicationReleaseNotes = applicationReleaseNotes; + } + + + /** + * Descriptor for {@link DescriptorImpl}. Used as a singleton. + * The class is marked as public so that it can be accessed from views. + * + *

+ * See src/main/resources/hudson/plugins/hello_world/HelloWorldBuilder/*.jelly + * for the actual HTML fragment for the configuration screen. + */ + @Extension + public static class DescriptorImpl extends Descriptor { + + /** + * Necessary Object to read the values entered in the GlobalConfigurationScreen. + */ + @Inject + GlobalConfigurationImpl globalConfiguration; + + @Override + public String getDisplayName() { + return ""; + } + + /** + * Fills the DropDownList on the configuration page of an job. The values are read out of the GlobalConfiguration. + * @return List of URLs which are entered in the GlobalConfigurationScreen + */ + public ListBoxModel doFillApiEndpointURLItems() { + Map loginCredentials = this.globalConfiguration.getLoginCredentials(); + final ListBoxModel items = new ListBoxModel(); + try { + for (Map.Entry entry : loginCredentials.entrySet()) { + items.add(entry.getKey().toString()); + } + } + catch(Exception ex) { + ex.printStackTrace(); + } + return items; + } + + /** + * Validates if the enntry in the Inputfield is empty/not empty. + * @param value value of the input field applicationName. + * @return true if an entry exists, false if no entry exists. + * @throws IOException + * @throws ServletException + */ + public FormValidation doCheckApplicationName(@QueryParameter final String value) + throws IOException, ServletException { + if (value.length() == 0) { + return FormValidation.error(Messages.Relution_appName()); + } + return FormValidation.ok(); + } + + /** + * Validates if the entry in the Inputfield is empty/no empty. + * @param value value of the input field applicationIcon. + * @return true if an entry exists, false if no entry exists. + * @throws IOException + * @throws ServletException + */ + public FormValidation doCheckApplicationIcon(@QueryParameter final String value) + throws IOException, ServletException { + if (value.length() == 0) { + return FormValidation.error(Messages.Relution_apiIconIsRequired()); + } + return FormValidation.ok(); + } + + /** + * @return List of ReleaseStatuses that the actual app could have. + */ + public ListBoxModel doFillApiReleaseStatusItems() { + final ListBoxModel items = new ListBoxModel(); + items.add("DEVELOPMENT"); + items.add("REVIEW"); + items.add("RELEASE"); + return items; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/relution/ApplicationFileCallable.java b/src/main/java/org/jenkinsci/plugins/relution/ApplicationFileCallable.java new file mode 100644 index 0000000..8dbfc55 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/relution/ApplicationFileCallable.java @@ -0,0 +1,117 @@ +package org.jenkinsci.plugins.relution; + +import hudson.FilePath.FileCallable; +import hudson.Util; +import hudson.model.BuildListener; +import hudson.model.Result; +import hudson.model.AbstractBuild; +import hudson.remoting.VirtualChannel; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.UUID; + +import org.apache.http.ParseException; +import org.apache.tools.ant.types.FileSet; +import org.jenkinsci.plugins.relution.entities.ApplicationInformation; +import org.jenkinsci.plugins.relution.entities.ShortApplicationInformation; +import org.jenkinsci.plugins.relution.json.APIObject; +import org.jenkinsci.plugins.relution.net.RequestFactory; + +@SuppressWarnings("serial") +public class ApplicationFileCallable implements FileCallable { + + @SuppressWarnings("rawtypes") + private final AbstractBuild build; + private final BuildListener listener; + private final Application application; + private final List communicators; + + /** + * Constructor. + * @param build Actual build number. + * @param listener Receives events that happen during a build. + * @param communicators List of all global specified communicators. + * @param application Job that will be build. + */ + @SuppressWarnings("rawtypes") + public ApplicationFileCallable(final AbstractBuild build, + final BuildListener listener, final List communicators, + final Application application) { + this.build = build; + this.listener = listener; + this.application = application; + this.communicators = communicators; + } + + /** + * These method is responsible to get the file to upload against the relution store during the build process. + * + *

+ * If the file could not be found the build will marked as failed! + * After the file is found retrieving short Information of the file + * In the last steps different request's will be created to + * - after creating the first request an APIObject File Object will be returned in the reponse + * - in the second step an request will created to get an APIObject App + * - Returns the existing APIObject App if resource with package name (internalName) already exists. + * - Returns an error if APIObject Version with versionCode already exists. + * - the last step will upload the APIObject App created in step 2 + * @param f Holds information about path the project relates to. + * @param channel Represents a communication channel to the remote/local peer. + * @return true if build executes fine, false else + */ + public Boolean invoke(final File f, final VirtualChannel channel) throws IOException, InterruptedException { + ShortApplicationInformation app = null; + final FileSet fileSet = Util.createFileSet(f, this.application.getApplicationFile()); + + if (fileSet.getDirectoryScanner().getIncludedFilesCount() < 1) { + this.listener.getLogger().println("[Relution Publisher]: Error, no files to deploy"); + this.build.setResult(Result.UNSTABLE); + + return false; + } + + final File applicationFile = new File(fileSet.getDirectoryScanner().getBasedir().getAbsolutePath() + File.separator + fileSet.getDirectoryScanner().getIncludedFiles()[0]); + try { + // ----------------------------------------------------------------- + // if(this.application.getApplicationUUID() == null) { + // this.application.setApplicationUUID(UUID.randomUUID().toString()); + // } + app = new ShortApplicationInformation(UUID.randomUUID().toString()); + if (app == null || app.getUUID() == null) { + this.listener.getLogger().println("[Relution Publisher]: Could not obtain application uuid"); + return false; + } + this.listener.getLogger().println("[Relution Publisher]: Obtained application UUID: " + app.getUUID()); + for(int index = 0; index < communicators.size(); index ++) { + RelutionCommunicator communicator = communicators.get(index); + if(communicator.getRequestFactory().getRelutionApiUrl().equals(this.application.getApiEndpointURL())) { + // app = communicator.getShortApplicationInformation(this.application.getApplicationUUID()); + // ----------------------------------------------------------------- + final String uploadedAssetToken = communicator.uploadApplicationAsset("", applicationFile); + // this.listener.getLogger().println("[Relution Publisher]: Uploaded application asset (token " + uploadedAssetToken + ")"); + // ----------------------------------------------------------------- + final String appObject = communicator.analyzeUploadedApplication(uploadedAssetToken, app.getUUID(), this.application.getApplicationIcon(), this.application.getApiReleaseStatus(), this.application.getApplicationName(), this.application.getApplicationReleaseNotes(), fileSet, communicator); + if(appObject.equals("Version already exists. Please delete the old one to upload the same version again.")) { + this.listener.getLogger().println("[Relution Publisher]: Version already exists. Please delete the old one to upload the same version again."); + return false; + } + // ----------------------------------------------------------------- + final ApplicationInformation information = communicator.saveApplicationInformation(appObject); + // this.listener.getLogger().println("[Relution Publisher]: Obtained information for uploaded application asset ( " + information + " )"); + this.listener.getLogger().println("[Relution Publisher]: Upload app with name " + applicationFile.getName()); + } + } + return true; + } catch (final ParseException e) { + e.printStackTrace(); + } catch (final URISyntaxException e) { + e.printStackTrace(); + } + + return false; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/relution/GlobalConfigurationImpl.java b/src/main/java/org/jenkinsci/plugins/relution/GlobalConfigurationImpl.java new file mode 100644 index 0000000..d516f82 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/relution/GlobalConfigurationImpl.java @@ -0,0 +1,363 @@ + +package org.jenkinsci.plugins.relution; + +import hudson.Extension; +import hudson.FilePath; +import hudson.model.AbstractProject; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.ServletException; + +import jenkins.model.GlobalConfiguration; +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; + +import org.apache.commons.validator.routines.UrlValidator; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.util.EntityUtils; +import org.jenkinsci.plugins.relution.net.Request; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; + + +@Extension +public class GlobalConfigurationImpl extends GlobalConfiguration { + + private List instances = new ArrayList(); + private static final String LOGIN_API_V1 = "/gofer/security-login"; + private static final String USERNAME_PARAM = "j_username"; + private static final String ORGANIZATION_PARAM = "j_organization"; + private static final String PASSWORD_PARAM = "j_password"; + private final static String LOGINREGEX = "^http\\://[a-zA-Z0-9\\-\\.]*"; + private final static String UUIDPattern = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"; + private Map loginCredentials = new HashMap(); + private String apiEndpoint; + private String apiUsername; + private String apiPassword; + private String proxyHost; + private int proxyPort; + private String apiOrganization; + private String response; + private String apiReleaseStatus; + + /** + * executed during startup of the Plugin and instantiate different GlobalConfiguration-Objects. + */ + public GlobalConfigurationImpl() { + this.load(); + } + + /** + * These constructor will be executed every time when the save/submit button will be triggered in the Jenkins. + * @param apiEndpoint URL to which the app should communicate + * @param apiUsername User which should be logged in. + * @param apiPassword Password for the given User. + * @param apiOrganization Organization for the given User. + * @param apiReleaseStatus ReleaseStatus to the given App + * @param proxyHost + * @param proxyPort + */ + @DataBoundConstructor + public GlobalConfigurationImpl(final String apiEndpoint, final String apiUsername, final String apiPassword, final String apiOrganization, final String apiReleaseStatus, final String proxyHost, final int proxyPort) { + super(); + this.load(); + + this.setApiEndpoint(apiEndpoint); + this.setApiUsername(apiUsername); + this.setApiPassword(apiPassword); + this.setApiOrganization(apiOrganization); + this.setApiReleaseStatus(apiReleaseStatus); + this.setProxyHost(proxyHost); + this.setProxyPort(proxyPort); + } + + /** + * Performs on-the-fly validation of the form field 'name'. + * @param value This parameter receives the value that the user has typed. + * @return Indicates the outcome of the validation. This is sent to the browser. + */ + public FormValidation doCheckApiEndpoint(@QueryParameter final String value) + throws IOException, ServletException { + if (value.length() == 0) { + return FormValidation.error(Messages.Relution_apiEndpointUrlIsRequired()); + } + + final String[] schemes = {"http", "https"}; + final UrlValidator validator = new UrlValidator(schemes); + if (!validator.isValid(value)) { + return FormValidation.error(Messages.Relution_apiEndpointUrlIsInvalid()); + } + + return FormValidation.ok(); + } + + /** + * Performs on-the-fly validation of the form field 'applicationFile'. + * @param value This parameter receives the value that the user has typed. + * @return Indicates the outcome of the validation. This is sent to the browser. + */ + @SuppressWarnings("rawtypes") + public FormValidation doCheckApplicationFile(@AncestorInPath final AbstractProject project, @QueryParameter final String value) + throws IOException, ServletException { + return FilePath.validateFileMask(project.getSomeWorkspace(), value); + } + + /** + * Performs on-the-fly validation of the form field 'applicationUUID'. + * @param value This parameter receives the value that the user has typed. + * @return Indicates the outcome of the validation. This is sent to the browser. + */ + public FormValidation doCheckApplicationUUID(@QueryParameter final String value) { + // Valid UUID: ac8a68b2-e2d1-459a-a6ff-3504a6f9c7ac + if (!value.matches(UUIDPattern)) { + return FormValidation.error(Messages.Relution_invalidApplicationUUID()); + } + + return FormValidation.ok(); + } + + /** + * List of the Statuses to an App. + * @return List with the statuses an app could have. + */ + public ListBoxModel doFillApiReleaseStatusItems() { + ListBoxModel items = new ListBoxModel(); + items.add("DEVELOPMENT"); + items.add("RELEASE"); + items.add("REVIEW"); + return items; + } + + /** + * Returns true if this GlobalConfiguration type is applicable to the given project. + */ + public boolean isApplicable(final Class aClass) { + return true; + } + + /** + * This human readable name is used in the configuration screen. + */ + @Override + public String getDisplayName() { + return Messages.Relution_displayName(); + } + + /** + * @return List of all saved fields entered in the GlobalConfigurationscreen in Jenkins. + */ + public List getInstances() { + return instances; + } + + /** + * @param sets an new instances added in the GlobalConfigurationscreen. + */ + public void setInstances(List instances) { + this.instances = instances; + } + + /** + * These method is responsible for the save the entered values in the GlobalConfigurationFields. + * @param req Request that should be send. + * @param formData Represents all fields parsed into an JSONObject. + * @return return value of the extended GlobalConfiguration Class. + */ + @Override + public boolean configure(final StaplerRequest req, final JSONObject formData) throws FormException { + Object apiEndpoint; + apiEndpoint = formData.get("apiEndpoint"); + if(apiEndpoint instanceof JSONArray) { + instances.clear(); + loginCredentials.clear(); + JSONArray jsonArray = formData.getJSONArray("apiEndpoint"); + for(int index = 0; index < jsonArray.size(); index++) { + this.apiEndpoint = ((JSONObject)jsonArray.get(index)).get("apiEndpoint").toString(); + this.apiUsername = ((JSONObject)jsonArray.get(index)).get("apiUsername").toString(); + this.apiPassword = ((JSONObject)jsonArray.get(index)).get("apiPassword").toString(); + this.apiOrganization = ((JSONObject)jsonArray.get(index)).get("apiOrganization").toString(); + this.apiReleaseStatus = ((JSONObject)jsonArray.get(index)).get("apiReleaseStatus").toString(); + this.proxyHost = formData.getString("proxyHost"); + this.proxyPort = formData.getInt("proxyPort"); + instances.add(new GlobalConfigurationImpl(this.apiEndpoint, this.apiUsername, this.apiPassword, this.apiOrganization, this.apiReleaseStatus, this.proxyHost, this.proxyPort)); + loginCredentials.put(((JSONObject)jsonArray.get(index)).get("apiEndpoint").toString(), this.apiUsername + ":" + this.apiOrganization + ":" + this.apiPassword); + } + } + else if(apiEndpoint instanceof JSONObject) { + instances.clear(); + loginCredentials.clear(); + JSONObject innerObject = (JSONObject) formData.get("apiEndpoint"); + this.apiEndpoint = innerObject.get("apiEndpoint").toString(); + this.apiUsername = innerObject.get("apiUsername").toString(); + this.apiPassword = innerObject.get("apiPassword").toString(); + this.apiOrganization = innerObject.get("apiOrganization").toString(); + this.apiReleaseStatus = innerObject.getString("apiReleaseStatus"); + this.proxyHost = formData.getString("proxyHost"); + this.proxyPort = formData.getInt("proxyPort"); + instances.add(new GlobalConfigurationImpl(this.apiEndpoint, this.apiUsername, this.apiPassword, this.apiOrganization, this.apiReleaseStatus, this.proxyHost, this.proxyPort)); + loginCredentials.put(this.apiEndpoint, this.apiUsername + ":" + this.apiOrganization + ":" + this.apiPassword); + } + System.out.println(formData.toString()); + + this.save(); + return super.configure(req, formData); + } + + /** + * Validates the given Credentials are right + * @param endpoint URL to connect to + * @param username User to connect to the specified URL + * @param organization Organization for the given User + * @param password Password for the given User + * @return Prints an dedicated statement to the User + * @throws IOException + * @throws ServletException + */ + public FormValidation doTestConnection(@QueryParameter("apiEndpoint") final String endpoint, + @QueryParameter("apiUsername") final String username, + @QueryParameter("apiOrganization") final String organization, + @QueryParameter("apiPassword") final String password) throws IOException, ServletException { + Pattern p = Pattern.compile(LOGINREGEX); + Matcher m = p.matcher(endpoint); + String loginURL = ""; + while (m.find()) { + loginURL = m.group(); + } + loginURL += LOGIN_API_V1 + "?" + USERNAME_PARAM + "="+ username + "&" + PASSWORD_PARAM + "=" + password + "&" + ORGANIZATION_PARAM + "=" + organization; + final Request request = new Request(Request.Method.GET, loginURL); + final DefaultHttpClient client = new DefaultHttpClient(); + try { + HttpRequestBase httpRequest = request.createHttpRequest(); + response = EntityUtils.toString(client.execute(httpRequest).getEntity(), Charset.forName("UTF-8")); + } catch (URISyntaxException e) { + return FormValidation.error(Messages.Relution_unsucessfullLogin() + endpoint + + " Errormessage: " + response); + } + if(response.equals("success")) { + return FormValidation.ok(Messages.Relution_successfulLogin() + endpoint); + } + else { + return FormValidation.error(Messages.Relution_unsucessfullLogin() + endpoint + " Errormessage: : " + response); + } + } + + + /** + * @return URL to which your app will connect. + */ + public String getApiEndpoint() { + return this.apiEndpoint; + } + + /** + * @param apiEndpointURL Communication endpoint to set. + */ + public void setApiEndpoint(final String apiEndpoint) { + this.apiEndpoint = apiEndpoint; + } + + /** + * @return Username used to login to relution. + */ + public String getApiUsername() { + return this.apiUsername; + } + + /** + * @param apiUsername Sets entry of the textfield apiUsername. + */ + public void setApiUsername(final String apiUsername) { + this.apiUsername = apiUsername; + } + + /** + * @return Password relates to the Username. + */ + public String getApiPassword() { + return this.apiPassword; + } + + /** + * @param apiPassword Sets entry of the textfield apiPassword. + */ + public void setApiPassword(final String apiPassword) { + this.apiPassword = apiPassword; + } + + /** + * @return Organization relates to the Username. + */ + public String getApiOrganization() { + return this.apiOrganization; + } + + /** + * @param apiOrganization Sets entry of the textfield apiOrganization. + */ + public void setApiOrganization(String apiOrganization) { + this.apiOrganization = apiOrganization; + } + + /** + * @return ProxyHost necessary for remote debugging + */ + public String getProxyHost() { + return this.proxyHost; + } + + /** + * @param proxyHost Sets entry of the textfield proxyHost. + */ + public void setProxyHost(final String proxyHost) { + this.proxyHost = proxyHost; + } + + /** + * @return ProxyPort necessary for remote debugging. + */ + public int getProxyPort() { + return this.proxyPort; + } + + /** + * @param proxyPort Sets entry of the textfield proxyPort. + */ + public void setProxyPort(final int proxyPort) { + this.proxyPort = proxyPort; + } + + /** + * @return Map with the entered login-Credentials. + */ + public Map getLoginCredentials() { + return this.loginCredentials; + } + + /** + * @param apiReleaseStatus Sets entry of the textfield apiReleaseStatus. + */ + public void setApiReleaseStatus(String apiReleaseStatus) { + this.apiReleaseStatus = apiReleaseStatus; + } + + /** + * @return actual entered ReleaseStatus. + */ + public String getApiReleaseStatus() { + return this.apiReleaseStatus; + } + } diff --git a/src/main/java/org/jenkinsci/plugins/relution/RelutionCommunicator.java b/src/main/java/org/jenkinsci/plugins/relution/RelutionCommunicator.java new file mode 100644 index 0000000..c048f29 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/relution/RelutionCommunicator.java @@ -0,0 +1,293 @@ +package org.jenkinsci.plugins.relution; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collection; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpHost; +import org.apache.http.ParseException; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.types.FileSet; +import org.jenkinsci.plugins.relution.entities.ApplicationInformation; +import org.jenkinsci.plugins.relution.json.APIObject; +import org.jenkinsci.plugins.relution.net.Request; +import org.jenkinsci.plugins.relution.net.RequestFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; + +public class RelutionCommunicator { + + private RequestFactory requestFactory; + + /** + * setting an Factory for an specific communicator + * @param requestFactory + */ + public void setRequestFactory(final RequestFactory requestFactory) { + this.requestFactory = requestFactory; + } + + /** + * getting the Factory an specific communicator + * @return getting the Factory an specific communicator + */ + public RequestFactory getRequestFactory() { + return this.requestFactory; + } + + /** + * Constructor to create a Communicator for Relution. + * @param apiEndpoint URL to which the communcator connects + * @param apiUsername login with the specified User + * @param apiPassword login with the specified Password + * @param apiOrganization login with the specified Organization + * @param apiReleaseStatus + * @param proxyHost necessary Host for remote debugging + * @param proxyPort necessary Port for remote debugging + * @param requestFactory handles the creation of the requests + */ + public RelutionCommunicator(final String apiEndpoint, final String apiUsername, + final String apiPassword, final String apiOrganization, + final String proxyHost, final String apiReleaseStatus, final int proxyPort, final RequestFactory requestFactory) { + requestFactory.setRelutionApiUrl(apiEndpoint); + requestFactory.setRelutionUsername(apiUsername); + requestFactory.setRelutionPassword(apiPassword); + requestFactory.setRelutionOrganization(apiOrganization); + requestFactory.setRelutionReleaseStatus(apiReleaseStatus); + requestFactory.setRelutionCookieStore(new BasicCookieStore()); + + if (proxyHost != null && !StringUtils.isEmpty(proxyHost) + && proxyPort > 0) { + requestFactory.setRelutionProxyHost(new HttpHost(proxyHost, proxyPort)); + } + this.setRequestFactory(requestFactory); + } + + /** + * Upload the application file and return the token for analyzing + * + * @param uploadToken + * @param file + * @return String + * @throws URISyntaxException + * @throws ParseException + * @throws ClientProtocolException + * @throws IOException + */ + public String uploadApplicationAsset(final String uploadToken, + final File file) throws URISyntaxException, ParseException, + ClientProtocolException, IOException { + + final Request request = this.getRequestFactory().createUploadRequest(uploadToken, file); + final String response = this.getRequestFactory().send(request); + + System.out.println("[uploadApplicationAsset] " + response); + + JsonParser parse = new JsonParser(); + JsonObject object = (JsonObject) parse.parse(response); + JsonArray apps = object.getAsJsonArray("results"); + String uuid = ""; + for (int i = 0; i < apps.size(); i++) { + JsonObject jsonObject = (JsonObject) apps.get(i); + uuid = jsonObject.get("uuid").getAsString(); + } + + return uuid.length() > 0 ? uuid : null; + } + + /** + * Analyze the uploaded application + * + * @param uploadToken + * @param UUID unique ID of the App that should be uploaded. + * @return Json Object that could be uploaded to the mcap. + * @throws URISyntaxException + * @throws IOException + * @throws ClientProtocolException + * @throws ParseException + */ + public String analyzeUploadedApplication(final String uploadToken, final String UUID, final String appIcon, final String releaseStatus, final String appName, final String appReleaseNotes, final FileSet fileSet, final RelutionCommunicator communicator) throws URISyntaxException, ParseException, + ClientProtocolException, IOException { + + final Request request = this.getRequestFactory().createAnalyzeUploadedApplication(uploadToken, UUID); + final String response = this.getRequestFactory().send(request); + + System.out.println("[analyzeUploadedApplication] " + response); + + JsonParser jsonStatusParser = new JsonParser(); + JsonObject jsonStatusObject = (JsonObject) jsonStatusParser.parse(response); + String status = jsonStatusObject.get("status").getAsString(); + if(status.equals("-1")) { + return jsonStatusObject.get("message").getAsString(); + } + else { + String changedApp = null; + if(appName != null && !appName.equals("")) { + changedApp = changeApplicationName(response, appName); + } + JsonParser jsonReleaseStatusParser = new JsonParser(); + if(changedApp != null) { + if(releaseStatus != null && !releaseStatus.equals("")) { + changedApp = changeReleaseStatus(changedApp, releaseStatus); + } + } + else { + if(releaseStatus != null && !releaseStatus.equals("")) { + changedApp = changeReleaseStatus(response, releaseStatus); + } + } + if(appIcon != null && !appIcon.equals("") || appReleaseNotes != null && !appReleaseNotes.equals("")) { + File applicationIconFile = null; + File applicationReleaseNotes = null; + DirectoryScanner scanner = new DirectoryScanner(); + scanner.setBasedir(fileSet.getDir()); + scanner.setCaseSensitive(false); + scanner.scan(); + String[] includedFiles = scanner.getIncludedFiles(); + for(int i = 0; i 256) { + log = log.substring(0, 252) + "..."; + } + changedApp = changeReleaseNotes(changedApp, log); + } + } + } + JsonObject jsonReleaseStatusObject = (JsonObject) jsonReleaseStatusParser.parse(changedApp); + JsonArray results = jsonReleaseStatusObject.getAsJsonArray("results"); + return results.get(0).toString(); + } + } + + private static String download(String url) throws java.io.IOException { + java.io.InputStream s = null; + String content = null; + try { + s = (java.io.InputStream)new URL(url).getContent(); + content = IOUtils.toString(s, "UTF-8"); + + } + finally { + if (s != null) s.close(); + } + return content.toString(); + } + + private static String changeReleaseNotes(String app, String appReleaseStatus) { + Gson gson = new Gson(); + APIObject.Changelog changeLog = new APIObject.Changelog(); + changeLog.setDe_DE(appReleaseStatus); + changeLog.setEn_US(appReleaseStatus); + + APIObject apiObject = gson.fromJson(app, APIObject.class); + for(int index=0; index>(){}.getType(); + Collection uploadedIconCollection = gson.fromJson(jsonIcon, collectionType); + APIObject.Icon icon = (APIObject.Icon)uploadedIconCollection.toArray()[0]; + APIObject apiObject = gson.fromJson(apiObjectResponse, APIObject.class); + for(int i=0; i applications = Collections.emptyList(); + private final static String UUIDPattern = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"; + private final static String LOGIN_REGEX = "([^:]*)"; + + /** + * Constructor. + * + * @param applicationFile + * @param applicationUUID + */ + @DataBoundConstructor + public RelutionPublisher(final List applications) { + this.getDescriptor().setInstances(applications); + this.applications = applications; + } + + /** + * Returns the application file path mask + * + * @return String + */ + public List getApplications() { + return this.applications; + } + + /** + * These method is the main entry point for the build-execution + * If the build with the actual number is already build without success the build will skipped + * After checking the build success retrieve the build-workspace and configure the Communicator + * The Communicator contains all necessary informations for the build and where to publish the file + * The last step is a check of the file field which must be present, if not the build will marked unsuccessful + * + * @param build Actual Build-Number + * @param launcher Starts the process + * @param listener The Listener is resposible for the Output in the jenkins Console-log + */ + @Override + public boolean perform(final AbstractBuild build, final Launcher launcher, final BuildListener listener) throws IOException, InterruptedException { + List communicators = new ArrayList(); + Map loginCredentials = this.getDescriptor().getGlobalConfiguration().getLoginCredentials(); + for(Application instance: applications) { + for(Map.Entry entry: loginCredentials.entrySet()) { + if(entry.getKey().equals(instance.getApiEndpointURL())) { + String[] credentials = entry.getValue().split(":"); + RelutionCommunicator communicator = new RelutionCommunicator(entry.getKey(), credentials[0], credentials[2], credentials[1], this.getDescriptor().getGlobalConfiguration().getProxyHost(), instance.getApiReleaseStatus(), this.getDescriptor().getGlobalConfiguration().getProxyPort(), new RequestFactory()); + communicators.add(communicator); + } + } + } + + if (build.getResult() != Result.SUCCESS) { + listener.getLogger().println("[Relution Publisher]: Skipping due to unsuccessful build"); + return true; + } + + final FilePath workspace = build.getWorkspace(); + for (final Application application : this.applications) { + + // Check that a file name pattern has been given + if (StringUtils.isEmpty(application.getApplicationFile())) { + build.setResult(Result.UNSTABLE); + listener.getLogger().println("[Relution Publisher]: ERROR - No application file mask defined"); + + return false; + } + + // Check that a valid application uuid has been provided + if (StringUtils.isEmpty(application.getApplicationFile()) || application.getApplicationFile().matches(UUIDPattern)) { + build.setResult(Result.UNSTABLE); + listener.getLogger().println("[Relution Publisher]: ERROR - No application file mask defined"); + + return false; + } + + final ApplicationFileCallable file = new ApplicationFileCallable(build, listener, communicators, application); + + workspace.act(file); + } + + return true; + } + + @Override + public DescriptorImpl getDescriptor() { + return (DescriptorImpl) super.getDescriptor(); + } + + /** + * Get an Monitor that monitors the whole build process + */ + public BuildStepMonitor getRequiredMonitorService() { + return BuildStepMonitor.NONE; + } + + /** + * Descriptor for {@link RelutionPublisher}. Used as a singleton. + * The class is marked as public so that it can be accessed from views. + * + *

+ * See src/main/resources/hudson/plugins/hello_world/HelloWorldBuilder/*.jelly + * for the actual HTML fragment for the configuration screen. + */ + @Extension + // This indicates to Jenkins that this is an implementation of an extension point. + public static final class DescriptorImpl extends BuildStepDescriptor { + + private List instances = new ArrayList(); + + /** + * Necessary Object to read the values entered in the GlobalConfigurationScreen. + */ + @Inject + GlobalConfigurationImpl globalConfiguration; + + /** + * @return List of all saved fields entered in the JobConfigurationscreen in Jenkins. + */ + public List getInstances() { + return instances; + } + + /** + * @param sets an new instances added in the JobConfigurationscreen. + */ + public void setInstances(List instances) { + this.instances = instances; + } + + /** + * load instance variables during startup. + */ + public DescriptorImpl() { + this.load(); + } + + @Override + public boolean isApplicable(final Class aClass) { + return true; + } + + @Override + public String getDisplayName() { + return Messages.Relution_displayName(); + } + + /** + * @return Actual GlobalConfiguration that contributes to the system configuration page. + */ + public GlobalConfigurationImpl getGlobalConfiguration() { + return this.globalConfiguration; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/relution/entities/ApplicationInformation.java b/src/main/java/org/jenkinsci/plugins/relution/entities/ApplicationInformation.java new file mode 100644 index 0000000..e265794 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/relution/entities/ApplicationInformation.java @@ -0,0 +1,25 @@ + +package org.jenkinsci.plugins.relution.entities; + +import java.util.Map; + + +public class ApplicationInformation { + + private Boolean published; + + /** + * holds information if the actual build application is published + * @return returns the actual published state of the application + */ + public boolean getPublished() { + return this.published; + } + + /** + * sets published information to the actual build application + */ + public void setPublished(boolean published) { + this.published = published; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/relution/entities/ApplicationList.java b/src/main/java/org/jenkinsci/plugins/relution/entities/ApplicationList.java new file mode 100644 index 0000000..91b0e20 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/relution/entities/ApplicationList.java @@ -0,0 +1,36 @@ + +package org.jenkinsci.plugins.relution.entities; + +import java.util.ArrayList; +import java.util.List; + + +public class ApplicationList { + + + private List results; + + /** + * Class holds informations about the released applications in the store + * @param list list of all Released Applications. + */ + public ApplicationList(ArrayList results) { + this.results = results; + } + + /** + * get an list of all released apps in the store. + * @return list of all released apps + */ + public List getResults() { + return results; + } + + /** + * set a new released app list + * @param results + */ + public void setResults(List results) { + this.results = results; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/relution/entities/ShortApplicationInformation.java b/src/main/java/org/jenkinsci/plugins/relution/entities/ShortApplicationInformation.java new file mode 100644 index 0000000..a7749dd --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/relution/entities/ShortApplicationInformation.java @@ -0,0 +1,21 @@ +package org.jenkinsci.plugins.relution.entities; + +public class ShortApplicationInformation { + + private String uuid; + + /** + * Holds Information of the app with the specific params. + * @param uuid distinct representation of the app. + */ + public ShortApplicationInformation(String uuid) { + this.uuid = uuid; + } + + /** + * @return String representation of an unique app-identifier. + */ + public String getUUID() { + return this.uuid; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/relution/json/APIObject.java b/src/main/java/org/jenkinsci/plugins/relution/json/APIObject.java new file mode 100644 index 0000000..a9df790 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/relution/json/APIObject.java @@ -0,0 +1,723 @@ +package org.jenkinsci.plugins.relution.json; + +import java.util.List; + +/** + * Object to cast an specific JSON to-from it and override given values with the specified setters/getters. + * + * @author Christian Steiger + */ +public class APIObject { + + private String status; + private String message; + private Errors errors; + private Integer total; + private List results; + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Errors getErrors() { + return errors; + } + + public void setErrors(Errors errors) { + this.errors = errors; + } + + public Integer getTotal() { + return total; + } + + public void setTotal(Integer total) { + this.total = total; + } + + public List getResults() { + return results; + } + + public void setResults(List results) { + this.results = results; + } + + public static class Errors { + } + + public static class Results { + + private String uuid; + private String type; + private String internalName; + private Object platforms; + private Object categories; + private List versions; + private String createdBy; + private Long creationDate; + private String modifiedBy; + private Long modificationDate; + private Integer rating; + private Integer ratingCount; + private Integer downloadCount; + private ACL acl; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getInternalName() { + return internalName; + } + + public void setInternalName(String internalName) { + this.internalName = internalName; + } + + public Object getPlatforms() { + return platforms; + } + + public void setPlatforms(Object platforms) { + this.platforms = platforms; + } + + public Object getCategories() { + return categories; + } + + public void setCategories(Object categories) { + this.categories = categories; + } + + public List getVersions() { + return versions; + } + + public void setVersions(List versions) { + this.versions = versions; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Long getCreationDate() { + return creationDate; + } + + public void setCreationDate(Long creationDate) { + this.creationDate = creationDate; + } + + public String getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(String modifiedBy) { + this.modifiedBy = modifiedBy; + } + + public Long getModificationDate() { + return modificationDate; + } + + public void setModificationDate(Long modificationDate) { + this.modificationDate = modificationDate; + } + + public Integer getRating() { + return rating; + } + + public void setRating(Integer rating) { + this.rating = rating; + } + + public Integer getRatingCount() { + return ratingCount; + } + + public void setRatingCount(Integer ratingCount) { + this.ratingCount = ratingCount; + } + + public Integer getDownloadCount() { + return downloadCount; + } + + public void setDownloadCount(Integer downloadCount) { + this.downloadCount = downloadCount; + } + + public ACL getAcl() { + return acl; + } + + public void setAcl(ACL acl) { + this.acl = acl; + } + } + + public static class Versions { + + private String uuid; + private String appUuid; + private String releaseStatus; + private String versionName; + private Integer versionCode; + private Integer downloadCount; + private Integer installCount; + private String link; + private File file; + private Object screenshots; + private Icon icon; + private Description description; + private Name name; + private Keywords keywords; + private Changelog changelog; + private List constraints; + private String copyright; + private String developerName; + private String developerWeb; + private String developerEmail; + private String createdBy; + private String creationDate; + private String modifiedBy; + private Long modificationDate; + private Integer rating; + private Integer ratingCount; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getAppUuid() { + return appUuid; + } + + public void setAppUuid(String appUuid) { + this.appUuid = appUuid; + } + + public String getReleaseStatus() { + return releaseStatus; + } + + public void setReleaseStatus(String releaseStatus) { + this.releaseStatus = releaseStatus; + } + + public String getVersionName() { + return versionName; + } + + public void setVersionName(String versionName) { + this.versionName = versionName; + } + + public Integer getVersionCode() { + return versionCode; + } + + public void setVersionCode(Integer versionCode) { + this.versionCode = versionCode; + } + + public Integer getDownloadCount() { + return downloadCount; + } + + public void setDownloadCount(Integer downloadCount) { + this.downloadCount = downloadCount; + } + + public Integer getInstallCount() { + return installCount; + } + + public void setInstallCount(Integer installCount) { + this.installCount = installCount; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } + + public File getFile() { + return file; + } + + public void setFile(File file) { + this.file = file; + } + + public Object getScreenshots() { + return screenshots; + } + + public void setScreenshots(Object screenshots) { + this.screenshots = screenshots; + } + + public Icon getIcon() { + return icon; + } + + public void setIcon(Icon icon) { + this.icon = icon; + } + + public Description getDescription() { + return description; + } + + public void setDescription(Description description) { + this.description = description; + } + + public Name getName() { + return name; + } + + public void setName(Name name) { + this.name = name; + } + + public Keywords getKeywords() { + return keywords; + } + + public void setKeywords(Keywords keywords) { + this.keywords = keywords; + } + + public Changelog getChangelog() { + return changelog; + } + + public void setChangelog(Changelog changelog) { + this.changelog = changelog; + } + + public List getConstraints() { + return constraints; + } + + public void setConstraints(List constraints) { + this.constraints = constraints; + } + + public String getCopyright() { + return copyright; + } + + public void setCopyright(String copyright) { + this.copyright = copyright; + } + + public String getDeveloperName() { + return developerName; + } + + public void setDeveloperName(String developerName) { + this.developerName = developerName; + } + + public String getDeveloperWeb() { + return developerWeb; + } + + public void setDeveloperWeb(String developerWeb) { + this.developerWeb = developerWeb; + } + + public String getDeveloperEmail() { + return developerEmail; + } + + public void setDeveloperEmail(String developerEmail) { + this.developerEmail = developerEmail; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public String getCreationDate() { + return creationDate; + } + + public void setCreationDate(String creationDate) { + this.creationDate = creationDate; + } + + public String getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(String modifiedBy) { + this.modifiedBy = modifiedBy; + } + + public Long getModificationDate() { + return modificationDate; + } + + public void setModificationDate(Long modificationDate) { + this.modificationDate = modificationDate; + } + + public Integer getRating() { + return rating; + } + + public void setRating(Integer rating) { + this.rating = rating; + } + + public Integer getRatingCount() { + return ratingCount; + } + + public void setRatingCount(Integer ratingCount) { + this.ratingCount = ratingCount; + } + } + + public static class File { + private String uuid; + private String name; + private String link; + private String contentType; + private Integer size; + private Long modificationDate; + private String downloadCount; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public Integer getSize() { + return size; + } + + public void setSize(Integer size) { + this.size = size; + } + + public Long getModificationDate() { + return modificationDate; + } + + public void setModificationDate(Long modificationDate) { + this.modificationDate = modificationDate; + } + + public String getDownloadCount() { + return downloadCount; + } + + public void setDownloadCount(String downloadCount) { + this.downloadCount = downloadCount; + } + } + + public static class Icon { + private String uuid; + private String name; + private String link; + private String contentType; + private Integer size; + private Long modificationDate; + private String downloadCount; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public Integer getSize() { + return size; + } + + public void setSize(Integer size) { + this.size = size; + } + + public Long getModificationDate() { + return modificationDate; + } + + public void setModificationDate(Long modificationDate) { + this.modificationDate = modificationDate; + } + + public String getDownloadCount() { + return downloadCount; + } + + public void setDownloadCount(String downloadCount) { + this.downloadCount = downloadCount; + } + } + + public static class Description { + private String en_US; + private String de_DE; + + public String getEn_US() { + return en_US; + } + + public void setEn_US(String en_US) { + this.en_US = en_US; + } + + public String getDe_DE() { + return de_DE; + } + + public void setDe_DE(String de_DE) { + this.de_DE = de_DE; + } + } + + public static class Name { + private String en_US; + private String de_DE; + + public String getEn_US() { + return en_US; + } + + public void setEn_US(String en_US) { + this.en_US = en_US; + } + + public String getDe_DE() { + return de_DE; + } + + public void setDe_DE(String de_DE) { + this.de_DE = de_DE; + } + } + + public static class Keywords { + private String en_US; + private String de_DE; + + public String getEn_US() { + return en_US; + } + + public void setEn_US(String en_US) { + this.en_US = en_US; + } + + public String getDe_DE() { + return de_DE; + } + + public void setDe_DE(String de_DE) { + this.de_DE = de_DE; + } + } + + public static class Changelog { + private String en_US; + private String de_DE; + + public String getEn_US() { + return en_US; + } + + public void setEn_US(String en_US) { + this.en_US = en_US; + } + + public String getDe_DE() { + return de_DE; + } + + public void setDe_DE(String de_DE) { + this.de_DE = de_DE; + } + } + + public static class Constraints { + private String uuid; + private String name; + private String value; + private String type; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + } + + public static class ACL { + private List RELEASE; + private List DEVELOPMENT; + private List REVIEW; + private List ARCHIV; + + public List getRELEASE() { + return RELEASE; + } + + public void setRELEASE(List RELEASE) { + this.RELEASE = RELEASE; + } + + public List getDEVELOPMENT() { + return DEVELOPMENT; + } + + public void setDEVELOPMENT(List DEVELOPMENT) { + this.DEVELOPMENT = DEVELOPMENT; + } + + public List getREVIEW() { + return REVIEW; + } + + public void setREVIEW(List REVIEW) { + this.REVIEW = REVIEW; + } + + public List getARCHIV() { + return ARCHIV; + } + + public void setARCHIV(List ARCHIV) { + this.ARCHIV = ARCHIV; + } + + } +} diff --git a/src/main/java/org/jenkinsci/plugins/relution/net/Request.java b/src/main/java/org/jenkinsci/plugins/relution/net/Request.java new file mode 100644 index 0000000..fdacf69 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/relution/net/Request.java @@ -0,0 +1,136 @@ + +package org.jenkinsci.plugins.relution.net; + +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + + +public class Request { + + private final RequestQueryFields mQueryFields = new RequestQueryFields(); + private final int mMethod; + private final String mUrl; + private final Map mHeaders = new HashMap(); + private HttpEntity mHttpEntity; + + /** + * Create an new Request Object + * @param method 0: GET, 1: POST, 2: PUT, 3: DELETE + * @param url specific url to which the request response + */ + public Request(final int method, final String url) { + this.mMethod = method; + this.mUrl = url; + } + + private HttpRequestBase createHttpRequest(final int method, final HttpEntity entity) { + + switch (method) { + default: + case Method.GET: + return new HttpGet(); + + case Method.POST: + final HttpPost post = new HttpPost(); + if (entity != null) { + post.setEntity(entity); + } + return post; + + case Method.PUT: + final HttpPut put = new HttpPut(); + if (entity != null) { + put.setEntity(entity); + } + return put; + + case Method.DELETE: + return new HttpDelete(); + } + } + + /** + * Add a key/value pair to the Requestheader. + * @param name key of the Headerfield. + * @param value value of the Headerfield. + */ + public void addHeader(final String name, final String value) { + this.mHeaders.put(name, value); + } + + /** + * Add a key/value pair in an specific format to the Requestheader. + * @param name key of the Headerfield. + * @param format arguments will be concatenated @see {@link String#format(String, Object...)} + * @param args values concatenated to a new value which will be added as a Headerfield. + */ + public void addHeader(final String name, final String format, final Object... args) { + final String value = String.format(format, args); + this.mHeaders.put(name, value); + } + + /** + * @return all querys appended to an specific URL. + */ + public RequestQueryFields queryFields() { + return this.mQueryFields; + } + + /** + * @return Returns the HttpEntity. + */ + public HttpEntity entity() { + return this.mHttpEntity; + } + + /** + * @param entity HttpEntity to be set. + */ + public void setEntity(final HttpEntity entity) { + this.mHttpEntity = entity; + } + + private String getUrl() { + if (this.mQueryFields.size() == 0) { + return this.mUrl; + } + return this.mUrl + this.mQueryFields.toString(); + } + + /** + * creation of a new Request (HttpGet, HttpPost, HttpPut, HttpDelete) + * with a (possibly) number of header values. + * @return new Request (HttpGet, HttpPost, HttpPut, HttpDelete). + * @throws URISyntaxException + */ + public HttpRequestBase createHttpRequest() throws URISyntaxException { + final HttpRequestBase request = this.createHttpRequest(this.mMethod, this.mHttpEntity); + + for (final String name : this.mHeaders.keySet()) { + request.addHeader(name, this.mHeaders.get(name)); + } + final URI uri = new URI(this.getUrl()); + request.setURI(uri); + + return request; + } + + /** + * Supported request methods. + */ + public interface Method { + public final static int GET = 0; + public final static int POST = 1; + public final static int PUT = 2; + public final static int DELETE = 3; + } +} \ No newline at end of file diff --git a/src/main/java/org/jenkinsci/plugins/relution/net/RequestFactory.java b/src/main/java/org/jenkinsci/plugins/relution/net/RequestFactory.java new file mode 100644 index 0000000..ad15991 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/relution/net/RequestFactory.java @@ -0,0 +1,232 @@ + +package org.jenkinsci.plugins.relution.net; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.Charset; + +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpHost; +import org.apache.http.HttpVersion; +import org.apache.http.ParseException; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.conn.params.ConnRoutePNames; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.MultipartEntity; +import org.apache.http.entity.mime.content.FileBody; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.CoreProtocolPNames; +import org.apache.http.util.EntityUtils; + + +public class RequestFactory { + + private final static Charset CHARSET = Charset.forName("UTF-8"); + private final static String URL_APP_STORE_ITEMS = "apps"; + private final static String URL_TEMP_FILE = "files"; + private final static String URL_RESOURCE_ANALYZER = "apps/fromFile"; + + private BasicCookieStore relutionCookieStore; + private HttpHost relutionProxyHost; + private String relutionApiUrl; + private String relutionUsername; + private String relutionPassword; + private String relutionOrganization; + private String relutionReleaseStatus; + + /** + * send the request with org.apache.http + * @param request request should be send + * @return response after executing request + * @throws URISyntaxException + * @throws ParseException + * @throws ClientProtocolException + * @throws IOException + */ + public String send(final Request request) throws URISyntaxException, ParseException, ClientProtocolException, IOException { + final DefaultHttpClient client = new DefaultHttpClient(); + + try { + if (relutionProxyHost != null) { + client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, relutionProxyHost); + } + client.getParams().setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1); + client.setCookieStore(relutionCookieStore); + + final HttpRequestBase httpRequest = request.createHttpRequest(); + System.out.println("Request > " + httpRequest.toString()); + + final String response = EntityUtils.toString(client.execute(httpRequest).getEntity(), CHARSET); + System.out.println("Response < " + response); + return response; + + } finally { + client.getConnectionManager().shutdown(); + } + } + + private String getAuthorizationToken(final String username, final String organization, String password) { + + final String authorization = username + ":" + organization + ":" + password; + return Base64.encodeBase64String(authorization.getBytes()); + } + + private String getUrl(final String path, final String... subs) { + + final StringBuilder sb = new StringBuilder(); + sb.append(relutionApiUrl); + sb.append(path); + + for (final String sub : subs) { + sb.append("/"); + sb.append(sub); + } + + return sb.toString(); + } + + private Request getBaseRequest(final int method, final String path, final String... subs) { + + final String url = getUrl(path, subs); + final Request request = new Request(method, url); + request.addHeader("Accept", "application/json"); + request.addHeader("Authorization", "Basic " + getAuthorizationToken(relutionUsername, relutionOrganization, getRelutionPassword())); + + return request; + } + + /** + * With these created request you could retrieve the apps stored in the relution store. + * @return json representation of the apps stored in the relution. + */ + public Request createAppStoreItemsRequest() { + final Request request = getBaseRequest(Request.Method.GET, URL_APP_STORE_ITEMS); + request.queryFields().add("locale", "de"); + return request; + } + + /** + * Creates an request which could be send against the relution. + * @param uploadToken empty string + * @param file file which should be created + * @return a new persisted APIObject File. + */ + public Request createUploadRequest(final String uploadToken, final File file) { + final Request request = getBaseRequest(Request.Method.POST, URL_TEMP_FILE, uploadToken); + final MultipartEntity entity = new MultipartEntity(); + entity.addPart("file", new FileBody(file)); + request.setEntity(entity); + return request; + } + + /** + * Creates an request which could be send against the relution. + * @param uploadToken empty String + * @param UUID uuid of the Object which should be checked + * @return APIObject App if the app not exists in the store or an error if the app already exists + */ + public Request createAnalyzeUploadedApplication(final String uploadToken, final String UUID) { + final Request request = getBaseRequest(Request.Method.POST, URL_RESOURCE_ANALYZER, uploadToken); + return request; + } + + /** + * Creates an request that could be send against the relution with an APIObject. + * @param appObject The APIObject returned in an previous step + * @return request could be send aginst the relution to upload the appObject + */ + public Request createUploadedApplicationInformationRequest(String appObject) { + final Request request = getBaseRequest(Request.Method.POST, URL_APP_STORE_ITEMS); + request.addHeader("Content-Type", "application/json"); + final StringEntity entity = new StringEntity(appObject, CHARSET); + request.setEntity(entity); + return request; + } + + /** + * @return Password relates to the Username. + */ + public String getRelutionPassword() { + return relutionPassword; + } + + /** + * @param relutionPassword Sets entry of the textfield apiPassword. + */ + public void setRelutionPassword(String relutionPassword) { + this.relutionPassword = relutionPassword; + } + + /** + * @return actual entered ReleaseStatus. + */ + public String getRelutionReleaseStatus() { + return relutionReleaseStatus; + } + + /** + * @param relutionReleaseStatus Sets entry of the textfield apiReleaseStatus. + */ + public void setRelutionReleaseStatus(String relutionReleaseStatus) { + this.relutionReleaseStatus = relutionReleaseStatus; + } + + /** + * @return URL to which your app will connect. + */ + public String getRelutionApiUrl() { + return relutionApiUrl; + } + + /** + * @param relutionApiUrl Communication endpoint to set. + */ + public void setRelutionApiUrl(String relutionApiUrl) { + this.relutionApiUrl = relutionApiUrl; + } + + /** + * @param relutionProxyHost HttpHost to be set. + */ + public void setRelutionProxyHost(final HttpHost relutionProxyHost) { + this.relutionProxyHost = relutionProxyHost; + } + + /** + * @param relutionCookieStore BasicCookieStore to set. + */ + public void setRelutionCookieStore(final BasicCookieStore relutionCookieStore) { + this.relutionCookieStore = relutionCookieStore; + } + + /** + * @return Username sed to login to relution. + */ + public String getRelutionUsername() { + return this.relutionUsername; + } + + /** + * @param relutionUsername Sets entry of the textfield apiUsername. + */ + public void setRelutionUsername(final String relutionUsername) { + this.relutionUsername = relutionUsername; + } + + /** + * @return Organization relates to the Username. + */ + public String getRelutionOrganization() { + return this.relutionOrganization; + } + + /** + * @param relutionOrganization Sets entry of the textfield apiOrganization. + */ + public void setRelutionOrganization(final String relutionOrganization) { + this.relutionOrganization = relutionOrganization; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/relution/net/RequestQueryFields.java b/src/main/java/org/jenkinsci/plugins/relution/net/RequestQueryFields.java new file mode 100644 index 0000000..bc60c55 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/relution/net/RequestQueryFields.java @@ -0,0 +1,124 @@ + +package org.jenkinsci.plugins.relution.net; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + + +public class RequestQueryFields { + + private final static String DEFAULT_CHARSET = "UTF-8"; + private final static String UNSUPPORTED_ENCODING = "The configured charset is unsupported on the current platform."; + private String mCharsetName = DEFAULT_CHARSET; + private final List mFields = new ArrayList(); + private String mQuery; + + private String encode(final String value) { + try { + return URLEncoder.encode(value, this.mCharsetName); + } catch (final UnsupportedEncodingException e) { + throw new IllegalStateException(UNSUPPORTED_ENCODING, e); + } + } + + /** + * Gets the character set used when encoding strings. + */ + public String getCharset() { + return this.mCharsetName; + } + + /** + * Specifies the character set to use when encoding strings. + * @param charsetName The name of the character set to use. + * @throws IllegalCharsetNameException if the specified name is illegal. + * @throws UnsupportedEncodingException if the specified character set is unsupported. + */ + public void setCharset(final String charsetName) throws UnsupportedEncodingException { + + if (!Charset.isSupported(charsetName)) { + throw new UnsupportedEncodingException("The specified charset is unsupported: " + charsetName); + } + this.mCharsetName = charsetName; + } + + /** + * adding key/value pair to an list which will be switched to an Query which could be append to an URL. + * @param name key of the parameter. + * @param value value of the parameter. + */ + public void add(final String name, final String value) { + if (name == null) { + throw new IllegalArgumentException("The specified argument cannot be null: name"); + } + + if (value == null || value.length() == 0) { + this.mFields.remove(name); + return; + } + + this.mFields.add(name); + this.mFields.add(value); + this.mQuery = null; + } + + /** + * @param name value to be removed from the List that could switched to an Query. + */ + public void remove(final String name) { + final int index = this.mFields.indexOf(name); + if (index % 2 == 0) { + this.mFields.remove(index); // key + this.mFields.remove(index); // value + } + this.mQuery = null; + } + + /** + * clear all instance variables. + */ + public void clear() { + this.mFields.clear(); + this.mQuery = null; + } + + /** + * @return size of the list divided by 2. + */ + public int size() { + return this.mFields.size() / 2; + } + + /** + * Switch the key/value Parameter of and list to an Query which could append to an URL. + */ + @Override + public String toString() { + if (this.mQuery == null) { + final StringBuilder sb = new StringBuilder(); + final int len = size(); + + for (int n = 0; n < len; n++) { + final String name = this.mFields.get(n * 2); + final String value = this.mFields.get(n * 2 + 1); + + if (sb.length() == 0) { + sb.append('?'); + } else { + sb.append('&'); + } + sb.append(this.encode(name)); + sb.append('='); + sb.append(this.encode(value)); + } + this.mQuery = sb.toString(); + } + return this.mQuery; + } +} diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly new file mode 100644 index 0000000..f280a59 --- /dev/null +++ b/src/main/resources/index.jelly @@ -0,0 +1,6 @@ + +

+ This plugin may be used to deploy build artifacts to the M-Way Solutions GmbH Relution Enterpriseappstore. +
diff --git a/src/main/resources/org/jenkinsci/plugins/relution/GlobalConfigurationImpl/config.jelly b/src/main/resources/org/jenkinsci/plugins/relution/GlobalConfigurationImpl/config.jelly new file mode 100644 index 0000000..4f77820 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/relution/GlobalConfigurationImpl/config.jelly @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + + +
+
+
+ + + + + + + + + + +
+
diff --git a/src/main/resources/org/jenkinsci/plugins/relution/Messages.properties b/src/main/resources/org/jenkinsci/plugins/relution/Messages.properties new file mode 100644 index 0000000..f03046d --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/relution/Messages.properties @@ -0,0 +1,8 @@ +Relution.displayName=Deploy to Relution Enterpriseappstore +Relution.apiEndpointUrlIsRequired=You must provide an endpoint URL +Relution.apiEndpointUrlIsInvalid=The provided URL is not valid +Relution.invalidApplicationUUID=This application UUID seems invalid +Relution.apiIconIsRequired=Please insert an icon +Relution.successfulLogin=You are now authenticated to URL: +Relution.unsucessfullLogin=The Login was not successfull to URL: +Relution.appName=Please a name for your app \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/relution/Messages_de.properties b/src/main/resources/org/jenkinsci/plugins/relution/Messages_de.properties new file mode 100644 index 0000000..777eee9 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/relution/Messages_de.properties @@ -0,0 +1,8 @@ +Relution.displayName=In Relution Enterpriseappstore veröffentlichen +Relution.apiEndpointUrlIsRequired=Sie müssen eine URL angeben +Relution.apiEndpointUrlIsInvalid=Die angegebene URL ist ungültig +Relution.invalidApplicationUUID=Diese UUID scheint nicht gültig zu sein +Relution.apiIconIsRequired=Bitte geben Sie ein Icon an +Relution.successfulLogin=Du bist erfolgreich authentifiziert zur URL: +Relution.unsucessfullLogin=Sorry aber der Login ist fehlgeschlagen zur URL: +Relution.appName=Bitte geben Sie ein Namen für die App an \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/relution/RelutionPublisher/config.jelly b/src/main/resources/org/jenkinsci/plugins/relution/RelutionPublisher/config.jelly new file mode 100644 index 0000000..76a3662 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/relution/RelutionPublisher/config.jelly @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+
diff --git a/src/main/webapp/help-apiEndpoint.html b/src/main/webapp/help-apiEndpoint.html new file mode 100644 index 0000000..114add9 --- /dev/null +++ b/src/main/webapp/help-apiEndpoint.html @@ -0,0 +1,3 @@ +
+ Please type here the URL to which you want upload your apps +
diff --git a/src/main/webapp/help-apiEndpointURL.html b/src/main/webapp/help-apiEndpointURL.html new file mode 100644 index 0000000..704f832 --- /dev/null +++ b/src/main/webapp/help-apiEndpointURL.html @@ -0,0 +1,3 @@ +
+ Please select here your Upload URL +
\ No newline at end of file diff --git a/src/main/webapp/help-apiOrganization.html b/src/main/webapp/help-apiOrganization.html new file mode 100644 index 0000000..e84d953 --- /dev/null +++ b/src/main/webapp/help-apiOrganization.html @@ -0,0 +1,3 @@ +
+ Please type here your Organization relates to the Username +
diff --git a/src/main/webapp/help-apiPassword.html b/src/main/webapp/help-apiPassword.html new file mode 100644 index 0000000..45d462a --- /dev/null +++ b/src/main/webapp/help-apiPassword.html @@ -0,0 +1,3 @@ +
+ Please type here your Password relates to the Username +
diff --git a/src/main/webapp/help-apiReleaseStatus.html b/src/main/webapp/help-apiReleaseStatus.html new file mode 100644 index 0000000..60b01fc --- /dev/null +++ b/src/main/webapp/help-apiReleaseStatus.html @@ -0,0 +1,3 @@ +
+ Please type here "DEVELOPMENT" or "RELEASE" or "REVIEW" +
\ No newline at end of file diff --git a/src/main/webapp/help-apiUsername.html b/src/main/webapp/help-apiUsername.html new file mode 100644 index 0000000..1930acd --- /dev/null +++ b/src/main/webapp/help-apiUsername.html @@ -0,0 +1,3 @@ +
+ Please type here your Username +
\ No newline at end of file diff --git a/src/main/webapp/help-applicationFile.html b/src/main/webapp/help-applicationFile.html new file mode 100644 index 0000000..ed6c8d7 --- /dev/null +++ b/src/main/webapp/help-applicationFile.html @@ -0,0 +1,5 @@ +
+ Here you specify which file should be uploaded to the URL you select. + You can use asterisk to upload all files with an specific ending. + The path you enter goes up from the root directory of your app. +
diff --git a/src/main/webapp/help-applicationIcon.html b/src/main/webapp/help-applicationIcon.html new file mode 100644 index 0000000..3f856dc --- /dev/null +++ b/src/main/webapp/help-applicationIcon.html @@ -0,0 +1,4 @@ +
+ Here you can type in the path to a picture which, after upload, will be represent your app in the Store. + The path goes up the root directory of your project. +
diff --git a/src/main/webapp/help-applicationName.html b/src/main/webapp/help-applicationName.html new file mode 100644 index 0000000..883fdb4 --- /dev/null +++ b/src/main/webapp/help-applicationName.html @@ -0,0 +1,3 @@ +
+ Here you can type in a name which represent your app in the Store. +
diff --git a/src/main/webapp/help-applicationReleaseNotes.html b/src/main/webapp/help-applicationReleaseNotes.html new file mode 100644 index 0000000..51dfa4c --- /dev/null +++ b/src/main/webapp/help-applicationReleaseNotes.html @@ -0,0 +1,3 @@ +
+ Please type here the the path to your textfile in which your logs are saved +