org.jenkinsci.plugins.relution_publisher.builder.ArtifactFileUploader.java Source code

Java tutorial

Introduction

Here is the source code for org.jenkinsci.plugins.relution_publisher.builder.ArtifactFileUploader.java

Source

/*
 * Copyright (c) 2013-2015 M-Way Solutions GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jenkinsci.plugins.relution_publisher.builder;

import com.google.common.base.Stopwatch;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import org.apache.commons.lang.StringUtils;
import org.apache.tools.ant.types.FileSet;
import org.jenkinsci.plugins.relution_publisher.configuration.global.Store;
import org.jenkinsci.plugins.relution_publisher.configuration.jobs.Publication;
import org.jenkinsci.plugins.relution_publisher.constants.ApiObject;
import org.jenkinsci.plugins.relution_publisher.constants.App;
import org.jenkinsci.plugins.relution_publisher.constants.ArchiveMode;
import org.jenkinsci.plugins.relution_publisher.constants.Language;
import org.jenkinsci.plugins.relution_publisher.constants.Version;
import org.jenkinsci.plugins.relution_publisher.logging.Log;
import org.jenkinsci.plugins.relution_publisher.net.RequestFactory;
import org.jenkinsci.plugins.relution_publisher.net.RequestManager;
import org.jenkinsci.plugins.relution_publisher.net.requests.ApiRequest;
import org.jenkinsci.plugins.relution_publisher.net.responses.ApiResponse;
import org.jenkinsci.plugins.relution_publisher.util.Builds;
import org.jenkinsci.plugins.relution_publisher.util.Json;
import org.jenkinsci.remoting.RoleChecker;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import hudson.FilePath.FileCallable;
import hudson.Util;
import hudson.model.Result;
import hudson.remoting.VirtualChannel;

/**
 * Uploads build artifacts to a {@link Store} that has been specified in a Jenkins project's
 * post-build action, in the form of a {@link Publication}.
 */
public class ArtifactFileUploader implements FileCallable<Boolean> {

    /**
     * The serial version number of this class.
     * <p>
     * This version number is used to determine whether a serialized representation of this class
     * is compatible with the current implementation of the class.
     * <p>
     * <b>Note</b> Maintainers must change this value <b>if and only if</b> the new version of this
     * class is not compatible with old versions.
     * @see
     * <a href="http://docs.oracle.com/javase/6/docs/platform/serialization/spec/version.html">
     * Versioning of Serializable Objects</a>.
     */
    private static final long serialVersionUID = 1L;

    /**
     * Maximum length of text to upload.
     */
    private static final int MAX_TEXT_LENGTH = 49152;

    private Result result;

    private final Publication publication;
    private final Store store;
    private final Log log;

    private Set<String> locales;

    private final RequestManager requestManager;

    /**
     * Initializes a new instance of the {@link ArtifactFileUploader} class.
     * @param result The build that produced the artifact to be published.
     * @param publication The {@link Publication} that describes the artifact to be published.
     * @param store The {@link Store} to which the publication should be published.
     * @param log The {@link Log} to write log messages to.
     */
    public ArtifactFileUploader(final Result result, final Publication publication, final Store store,
            final Log log) {

        this.result = result;

        this.publication = publication;
        this.store = store;
        this.log = log;

        this.requestManager = new RequestManager();
        this.requestManager.setProxy(store.getProxyHost(), store.getProxyPort());
        this.requestManager.setProxyCredentials(store.getProxyUsername(), store.getProxyPassword());
    }

    @Override
    public Boolean invoke(final File basePath, final VirtualChannel channel)
            throws IOException, InterruptedException {

        try {
            this.log.write(this, "Uploading build artifacts");
            final List<JsonObject> assets = this.uploadAssets(basePath, this.publication.getArtifactPath(),
                    this.publication.getArtifactExcludePath());

            if (this.isEmpty(assets) && this.result == Result.UNSTABLE) {
                this.log.write(this, "Upload of build artifacts failed.");
                return true;

            } else if (this.isEmpty(assets)) {
                this.log.write(this, "No artifacts to upload found.");
                Builds.setResult(this, Result.NOT_BUILT, this.log);
                return true;
            }

            for (final JsonObject asset : assets) {
                this.log.write();
                this.retrieveApplication(basePath, asset);
            }

        } catch (final IOException e) {
            this.log.write(this, "Publication failed.\n\n%s\n", e);
            Builds.setResult(this, Result.UNSTABLE, this.log);

        } catch (final URISyntaxException e) {
            this.log.write(this, "Publication failed.\n\n%s\n", e);
            Builds.setResult(this, Result.UNSTABLE, this.log);

        } catch (final ExecutionException e) {
            this.log.write(this, "Publication failed.\n\n%s\n", e);
            Builds.setResult(this, Result.UNSTABLE, this.log);

        } finally {
            this.requestManager.shutdown();

        }

        return true;
    }

    private void retrieveApplication(final File basePath, final JsonObject asset)
            throws URISyntaxException, IOException, InterruptedException, ExecutionException {

        this.log.write(this, "Requesting app associated with asset {%s}", Json.getString(asset, ApiObject.UUID));
        final ApiRequest request = RequestFactory.createAppFromFileRequest(this.store, asset);
        final ApiResponse response = this.requestManager.execute(request, this.log);

        if (!this.verifyApplicationResponse(response)) {
            this.log.write(this, "Retrieval of app failed.");
            Builds.setResult(this, Result.UNSTABLE, this.log);
            return;
        }

        final JsonArray applications = response.getResults();
        final JsonObject app = this.getApplication(applications, asset);

        if (Json.isNull(app)) {
            Builds.setResult(this, Result.UNSTABLE, this.log);
            this.log.write(this, "Could not find app associated with uploaded file.");
            return;
        }

        this.log.write(this, "App \"%s\" was retrieved.", Json.getString(app, App.INTERNAL_NAME));
        this.log.write(this, "Searching app version associated with uploaded file");
        final JsonObject version = this.getVersion(app, asset);

        if (Json.isNull(version)) {
            this.log.write(this, "Could not find app version associated with uploaded file.");
            Builds.setResult(this, Result.UNSTABLE, this.log);
            return;
        }

        this.log.write(this, "Found app version \"%s\".", Json.getString(version, Version.VERSION_NAME));
        this.setVersionMetadata(basePath, version);

        if (Json.isNull(app, ApiObject.UUID)) {
            this.persistApplication(app);

        } else {
            if (this.persistVersion(app, version)) {
                this.manageArchivedVersions(app, version);
            }
        }

        this.log.write(this, "Uploaded app version \"%s\" (%d) to \"%s\"",
                Json.getString(version, Version.VERSION_NAME), Json.getInt(version, Version.VERSION_CODE),
                Json.getString(version, Version.RELEASE_STATUS));
    }

    private List<JsonObject> getArchivedVersions(final JsonObject app, final JsonObject newVersion) {
        final String newReleaseStatus = Json.getString(newVersion, Version.RELEASE_STATUS);
        final int newVersionCode = Json.getInt(newVersion, Version.VERSION_CODE);

        final List<JsonObject> archived = new ArrayList<JsonObject>();
        final JsonArray versions = Json.getArray(app, App.VERSIONS);

        for (final JsonElement element : versions) {
            final JsonObject oldVersion = element.getAsJsonObject();

            final String oldReleaseStatus = Json.getString(oldVersion, Version.RELEASE_STATUS);
            final int oldVersionCode = Json.getInt(oldVersion, Version.VERSION_CODE);

            if (StringUtils.equals(oldReleaseStatus, newReleaseStatus) && oldVersionCode != newVersionCode) {
                archived.add(oldVersion);
            }
        }

        return archived;
    }

    private void manageArchivedVersions(final JsonObject app, final JsonObject version)
            throws URISyntaxException, InterruptedException, ExecutionException {

        final String archiveMode = !this.publication.usesDefaultArchiveMode() ? this.publication.getArchiveMode()
                : this.store.getArchiveMode();

        if (StringUtils.equals(archiveMode, ArchiveMode.OVERWRITE.key)) {
            this.log.write(this, "Delete previous app version from \"%s\"",
                    Json.getString(version, Version.RELEASE_STATUS));
            final List<JsonObject> archived = this.getArchivedVersions(app, version);

            for (final JsonObject current : archived) {
                this.deleteVersion(current);
            }

        } else {
            this.log.write(this, "Keep previous app version (moved to archive)");

        }
    }

    private void deleteVersion(final JsonObject version)
            throws URISyntaxException, InterruptedException, ExecutionException {
        this.log.write(this, "Deleting app version \"%s\" (%d) from \"%s\"",
                Json.getString(version, Version.VERSION_NAME), Json.getInt(version, Version.VERSION_CODE),
                Json.getString(version, Version.RELEASE_STATUS));

        try {
            final ApiRequest request = RequestFactory.createDeleteVersionRequest(this.store, version);
            final ApiResponse response = this.requestManager.execute(request, this.log);

            if (!this.verifyDeleteResponse(response)) {
                this.log.write(this, "Error deleting app version");
                Builds.setResult(this, Result.UNSTABLE, this.log);
                return;
            }

        } catch (final IOException e) {
            this.log.write(this, "Error deleting app version: %s", e.getMessage());
            e.printStackTrace();
        }
    }

    private void setVersionMetadata(final File basePath, final JsonObject version)
            throws URISyntaxException, IOException, InterruptedException, ExecutionException {

        this.setReleaseStatus(version);

        this.setName(version);
        this.setIcon(basePath, version);

        this.setChangeLog(basePath, version);
        this.setDescription(basePath, version);

        this.setVersionName(version);
    }

    private void setReleaseStatus(final JsonObject version) {
        final String releaseStatus = !this.publication.usesDefaultReleaseStatus()
                ? this.publication.getReleaseStatus()
                : this.store.getReleaseStatus();

        if (!StringUtils.isBlank(releaseStatus)) {
            version.addProperty("releaseStatus", releaseStatus);
        }
    }

    private void setName(final JsonObject version) throws IOException, InterruptedException, ExecutionException {
        if (StringUtils.isBlank(this.publication.getName())) {
            this.log.write(this, "No name set, default name will be used.");
            return;
        }

        this.setText("name", version.get(Version.NAME), this.publication.getName());
    }

    private void setIcon(final File basePath, final JsonObject version)
            throws URISyntaxException, IOException, InterruptedException, ExecutionException {

        if (StringUtils.isBlank(this.publication.getIconPath())) {
            this.log.write(this, "No icon set, default icon will be used.");
            return;
        }

        this.log.write(this, "Uploading app icon");
        final String filePath = this.publication.getIconPath();
        final List<JsonObject> assets = this.uploadAssets(basePath, filePath, null);

        if (assets.size() != 1) {
            this.log.write(this, "More than one unpersisted asset returned by server.");
            Builds.setResult(this, Result.UNSTABLE, this.log);
            return;
        }

        version.add("icon", assets.get(0));
    }

    private void setChangeLog(final File basePath, final JsonObject version)
            throws IOException, InterruptedException, ExecutionException {

        if (StringUtils.isBlank(this.publication.getChangeLogPath())) {
            this.log.write(this, "The change log path is empty, nothing to set.");
            return;
        }

        final String filePath = this.publication.getChangeLogPath();
        final String changeLogText = this.readFile(basePath, filePath);
        this.setText("change log", version.get(Version.CHANGE_LOG), changeLogText);
    }

    private void setDescription(final File basePath, final JsonObject version)
            throws IOException, InterruptedException, ExecutionException {

        if (StringUtils.isBlank(this.publication.getDescriptionPath())) {
            this.log.write(this, "The description path is empty, nothing to set.");
            return;
        }

        final String filePath = this.publication.getDescriptionPath();
        final String descriptionText = this.readFile(basePath, filePath);
        this.setText("description", version.get(Version.DESCRIPTION), descriptionText);
    }

    private void setText(final String item, final JsonElement element, final String text)
            throws IOException, InterruptedException, ExecutionException {
        if (StringUtils.isBlank(text)) {
            this.log.write(this, "The %s is empty, nothing to set.", item);
            return;
        }

        final String ellipsized = this.getEllipsizedText(text.replace("\n", "<br/>"), 50);
        this.log.write(this, "Set %s to: \"%s\" (%d characters)", item, ellipsized, text.length());

        // Add the text for each locale
        final JsonObject localizedString = element.getAsJsonObject();
        final Set<String> locales = this.getLocales();

        for (final String locale : locales) {
            this.log.write(this, "Set %s for locale %s.", item, locale);
            localizedString.addProperty(locale, text);
        }
    }

    private Set<String> getLocales() throws IOException, InterruptedException, ExecutionException {
        if (this.locales != null) {
            return this.locales;
        }

        this.log.write(this, "Requesting configured languages");

        final ApiRequest request = RequestFactory.createLanguageRequest(this.store);
        final ApiResponse response = this.requestManager.execute(request, this.log);

        final JsonArray languages = response.getResults();
        this.locales = new HashSet<String>(languages.size());

        for (final JsonElement element : languages) {
            final JsonObject language = element.getAsJsonObject();
            final String name = Json.getString(language, Language.NAME);
            final String locale = Json.getString(language, Language.LOCALE);

            this.log.write(this, "%s: %s", name, locale);
            this.locales.add(locale);
        }

        return this.locales;
    }

    private void setVersionName(final JsonObject version) {
        if (StringUtils.isBlank(this.publication.getVersionName())) {
            this.log.write(this, "No version name set, default name will be used.");
            return;
        }
        version.addProperty("versionName", this.publication.getVersionName());
    }

    private boolean persistApplication(final JsonObject app)
            throws URISyntaxException, IOException, InterruptedException, ExecutionException {
        this.log.write(this, "App is new, persisting app");

        final ApiRequest request = RequestFactory.createPersistApplicationRequest(this.store, app);
        final ApiResponse response = this.requestManager.execute(request, this.log);

        if (!this.verifyApplicationResponse(response)) {
            this.log.write(this, "Error persisting app.");
            Builds.setResult(this, Result.UNSTABLE, this.log);
            return false;
        }

        this.log.write(this, "App persisted successfully.");
        return true;
    }

    private boolean persistVersion(final JsonObject app, final JsonObject version)
            throws URISyntaxException, IOException, InterruptedException, ExecutionException {
        this.log.write(this, "App version is new, persisting app version");

        final ApiRequest request = RequestFactory.createPersistVersionRequest(this.store, app, version);
        final ApiResponse response = this.requestManager.execute(request, this.log);

        if (!this.verifyApplicationResponse(response)) {
            this.log.write(this, "Error persisting app version.");
            Builds.setResult(this, Result.UNSTABLE, this.log);
            return false;
        }

        this.log.write(this, "App version persisted successfully.");
        return true;
    }

    private List<JsonObject> uploadAssets(final File basePath, final String includes, final String excludes)
            throws URISyntaxException, InterruptedException {

        if (StringUtils.isBlank(includes)) {
            this.log.write(this, "No file to upload specified, filter expression is empty, upload failed.");
            return null;
        }

        if (!StringUtils.isBlank(excludes)) {
            this.log.write(this, "Excluding files that match \"%s\"", excludes);
        }

        final FileSet fileSet = Util.createFileSet(basePath, includes, excludes);
        final File directory = fileSet.getDirectoryScanner().getBasedir();

        if (fileSet.getDirectoryScanner().getIncludedFilesCount() < 1) {
            this.log.write(this, "The file specified by \"%s\" does not exist, upload failed.", includes);
            return null;
        }

        final List<JsonObject> assets = new ArrayList<JsonObject>();

        for (final String fileName : fileSet.getDirectoryScanner().getIncludedFiles()) {
            final JsonObject asset = this.uploadAsset(directory, fileName);

            if (asset != null) {
                assets.add(asset);
            }
        }

        return assets;
    }

    private JsonObject uploadAsset(final File directory, final String fileName)
            throws URISyntaxException, InterruptedException {

        try {
            final Stopwatch sw = new Stopwatch();
            final File file = new File(directory, fileName);
            final ApiRequest request = RequestFactory.createUploadRequest(this.store, file);

            this.log.write(this, "Uploading \"%s\" (%,d Byte)", fileName, file.length());

            sw.start();
            final ApiResponse response = this.requestManager.execute(request, this.log);
            sw.stop();

            final String speed = this.getUploadSpeed(sw, file);
            this.log.write(this, "Upload of file completed (%s, %s).", sw, speed);

            return this.extractAsset(response);

        } catch (final IOException e) {
            this.log.write(this, "Upload of file failed, error during execution:\n\n%s\n", e);
            Builds.setResult(this, Result.UNSTABLE, this.log);

        } catch (final ExecutionException e) {
            this.log.write(this, "Upload of file failed, error during execution:\n\n%s\n", e);
            Builds.setResult(this, Result.UNSTABLE, this.log);

        }
        return null;
    }

    private JsonObject extractAsset(final ApiResponse response) {
        if (response == null) {
            this.log.write(this, "Error during upload, server's response is empty.");
            return null;
        }

        if (!this.verifyAssetResponse(response)) {
            this.log.write(this, "Upload of asset failed.");
            Builds.setResult(this, Result.UNSTABLE, this.log);
            return null;
        }

        final JsonArray assets = response.getResults();

        if (assets.size() != 1) {
            this.log.write(this, "Error during upload, more than one asset returned by server.");
            Builds.setResult(this, Result.UNSTABLE, this.log);
            return null;
        }

        final JsonObject asset = Json.getObject(assets, 0);

        if (Json.isNull(asset)) {
            this.log.write(this, "Error during upload, asset is null.");
            Builds.setResult(this, Result.UNSTABLE, this.log);
            return null;
        }

        this.log.write(this, "Upload completed, received asset {%s}", Json.getString(asset, ApiObject.UUID));
        return asset;
    }

    private String getUploadSpeed(final Stopwatch sw, final File file) {
        final float milliseconds = sw.elapsedTime(TimeUnit.MILLISECONDS);
        final float seconds = milliseconds / 1000f;

        if (file.length() == 0 || seconds == 0) {
            return "Unknown";
        }

        final String[] units = { "", "K", "M", "G" };

        float speed = file.length() / seconds;
        int index = 0;

        while (speed > 2048 && index < units.length) {
            speed /= 1024;
            ++index;
        }

        return String.format("%,.0f %sB/s", speed, units[index]);
    }

    private String readFile(final File basePath, final String filePath) {

        final FileSet fileSet = Util.createFileSet(basePath, filePath);
        final File directory = fileSet.getDirectoryScanner().getBasedir();
        final StringBuilder sb = new StringBuilder();

        if (fileSet.getDirectoryScanner().getIncludedFilesCount() < 1) {
            this.log.write(this, "The file specified by \"%s\" does not exist.", filePath);
        }

        for (final String fileName : fileSet.getDirectoryScanner().getIncludedFiles()) {
            this.log.write(this, "Reading file \"%s\"", fileName);
            final File file = new File(directory, fileName);
            this.readFile(file, sb);
        }
        return this.getEllipsizedText(sb.toString(), MAX_TEXT_LENGTH);
    }

    private void readFile(final File file, final StringBuilder sb) {

        try {
            final BufferedReader br = new BufferedReader(new FileReader(file));
            String line;

            while ((line = br.readLine()) != null && sb.length() < MAX_TEXT_LENGTH) {
                sb.append(line);
                sb.append("\n");
            }

            if (sb.length() >= MAX_TEXT_LENGTH) {
                this.log.write(this, "Text in file \"%s\" exceeds %d characters and will be truncated.",
                        file.getName(), MAX_TEXT_LENGTH);
            }

            br.close();

        } catch (final FileNotFoundException e) {
            e.printStackTrace();
        } catch (final IOException e) {
            e.printStackTrace();
        }
    }

    private JsonObject getApplication(final JsonArray applications, final JsonObject asset) {
        for (final JsonElement element : applications) {
            final JsonObject app = element.getAsJsonObject();
            final JsonObject version = this.getVersion(app, asset);

            if (version != null) {
                return app;
            }
        }
        return null;
    }

    private JsonObject getVersion(final JsonObject app, final JsonObject asset) {
        final String uuid = Json.getString(asset, ApiObject.UUID);
        final JsonArray versions = Json.getArray(app, App.VERSIONS);

        for (final JsonElement element : versions) {
            final JsonObject version = element.getAsJsonObject();
            final JsonObject file = Json.getObject(version, Version.FILE);

            if (file != null) {
                final String fileUuid = Json.getString(file, ApiObject.UUID);

                if (StringUtils.equals(fileUuid, uuid)) {
                    return version;
                }
            }
        }
        return null;
    }

    private boolean verifyAssetResponse(final ApiResponse response) {
        final JsonArray assets = response.getResults();

        if (response.getStatus() != 0) {
            this.log.write(this, "Error uploading file (%d), server's response:\n\n%s\n", response.getStatusCode(),
                    response.getMessage());

            return false;
        }

        if (Json.isEmpty(assets)) {
            this.log.write(this, "Error uploading file, the server returned no assets.");
            return false;
        }

        return true;
    }

    private boolean verifyApplicationResponse(final ApiResponse response) {
        final JsonArray applications = response.getResults();

        if (response.getStatus() != 0) {
            this.log.write(this, "Error creating app (%d), server's response:\n\n%s\n", response.getStatusCode(),
                    response.getMessage());

            return false;
        }

        if (Json.isEmpty(applications)) {
            this.log.write(this, "Error creating app, the server returned no apps.");
            return false;
        }

        return true;
    }

    private boolean verifyDeleteResponse(final ApiResponse response) {
        this.log.write(this, "Status: %d", response.getStatus());

        if (response.getStatus() != 0) {
            this.log.write(this, "Error deleting app version (%d), server's response:\n\n%s\n",
                    response.getStatusCode(), response.getMessage());

            return false;
        }

        return true;
    }

    private String getEllipsizedText(final String input, final int maxLen) {
        if (StringUtils.isEmpty(input) || input.length() <= maxLen) {
            return input;
        }
        return input.substring(0, maxLen - 1) + "";
    }

    private boolean isEmpty(final Collection<?> collection) {
        return (collection == null || collection.size() == 0);
    }

    public Result getResult() {
        return this.result;
    }

    public void setResult(final Result result) {
        this.result = result;
    }

    @Override
    public void checkRoles(final RoleChecker roleChecker) throws SecurityException {
    }
}