diff --git a/kobalt/src/Build.kt b/kobalt/src/Build.kt index 2388fe60..b790fe9f 100644 --- a/kobalt/src/Build.kt +++ b/kobalt/src/Build.kt @@ -132,8 +132,9 @@ val kobaltApp = project(kobaltPluginApi, wrapper) { "com.google.code.findbugs:jsr305:3.0.1", "com.google.code.gson:gson:${Versions.gson}", "com.squareup.okhttp3:okhttp:${Versions.okhttp}", + "com.squareup.retrofit2:retrofit:${Versions.retrofit}", + "com.squareup.retrofit2:converter-gson:${Versions.retrofit}", "org.codehaus.plexus:plexus-utils:3.0.22", - "biz.aQute.bnd:bndlib:2.4.0" ) diff --git a/src/main/kotlin/com/beust/kobalt/plugin/publish/BintrayApi.kt b/src/main/kotlin/com/beust/kobalt/plugin/publish/BintrayApi.kt index cae618ed..8d5ee4da 100644 --- a/src/main/kotlin/com/beust/kobalt/plugin/publish/BintrayApi.kt +++ b/src/main/kotlin/com/beust/kobalt/plugin/publish/BintrayApi.kt @@ -10,99 +10,139 @@ import com.beust.kobalt.misc.KobaltExecutors import com.beust.kobalt.misc.error import com.beust.kobalt.misc.log import com.beust.kobalt.misc.warn -import com.google.common.net.MediaType +import com.google.gson.JsonArray import com.google.gson.JsonObject -import com.google.gson.JsonParser import com.google.inject.assistedinject.Assisted -import okhttp3.Response +import okhttp3.Credentials +import okhttp3.Interceptor +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.RequestBody +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Part +import retrofit2.http.Path import java.io.File import javax.annotation.Nullable import javax.inject.Inject -data class BintrayPackage(val jo: JsonObject) { -// @Suppress("UNCHECKED_CAST") -// val latestPublishedVersion = (jo.get("versions") as JsonArray).get(0) as JsonObject). -} +class BintrayResponse() + +class BintrayApi @Inject constructor(val http: Http, + @Nullable @Assisted("username") val username: String?, + @Nullable @Assisted("password") val password: String?, + @Nullable @Assisted("org") val org: String?, + val gpg: Gpg, val executors: KobaltExecutors) { -open public class UnauthenticatedBintrayApi @Inject constructor(open val http: Http) { companion object { const val BINTRAY_URL_API = "https://api.bintray.com" - const val BINTRAY_URL_API_CONTENT = BINTRAY_URL_API + "/content" } - class BintrayResponse(val jo: JsonObject?, val errorMessage: String?) - - fun parseResponse(r: Response): BintrayResponse { - val networkResponse = r.networkResponse() - if (networkResponse.code() != 200) { - val message = networkResponse.message() - try { - val errorObject = JsonParser().parse(r.body().string()).asJsonObject - return BintrayResponse(null, message + ": " + errorObject.get("message").asString) - } catch(ex: Exception) { - return BintrayResponse(null, message) - } - } else { - return BintrayResponse(JsonParser().parse(r.body().string()).asJsonObject, null) - } - } -} - -class BintrayApi @Inject constructor ( - @Nullable @Assisted("username") val username: String?, - @Nullable @Assisted("password") val password: String?, - @Nullable @Assisted("org") val org: String?, - override val http: Http, val gpg: Gpg, val executors: KobaltExecutors) : UnauthenticatedBintrayApi(http) { - interface IFactory { fun create(@Nullable @Assisted("username") username: String?, @Nullable @Assisted("password") password: String?, - @Nullable @Assisted("org") org: String?) : BintrayApi + @Nullable @Assisted("org") org: String?): BintrayApi } - fun packageExists(packageName: String) : Boolean { - val url = arrayListOf(UnauthenticatedBintrayApi.BINTRAY_URL_API, "packages", org ?: username!!, - "maven", packageName) - .joinToString("/") - val jcResponse = parseResponse(http.get(username, password, url)) + interface Api { + @GET("/packages/{owner}/maven/{package}") + fun getPackage(@Path("owner") owner: String, + @Path("package") name: String): Call + + @POST("/packages/{owner}/maven") + fun createPackage(@Path("owner") owner: String, + @Body content: JsonObject): Call + + @Multipart + @Headers("Content-Type: application/xml") + @PUT("/content/{owner}/maven/{repo}/{version}/{group}/{artifact}/{version}/{name}") + fun uploadPom(@Path("owner") owner: String, + @Path("repo") repo: String, + @Path("version") version: String, + @Path("group", encoded = true) group: String, + @Path("artifact") artifact: String, + @Path("name") name: String, + @Part file: MultipartBody.Part): Call + + @Multipart + @PUT("/maven/{owner}/maven/{package}/{group}/{artifact}/{version}/{name}") + fun uploadArtifact(@Path("owner") owner: String, + @Path("package") bintrayPackage: String, + @Path("group", encoded = true) group: String, + @Path("artifact") artifact: String, + @Path("version") version: String, + @Path("name") name: String, + @Part file: MultipartBody.Part): Call - if (jcResponse.errorMessage != null) { - throw KobaltException("Error from Bintray: ${jcResponse.errorMessage}") - } - return jcResponse.jo!!.get("name").asString == packageName } - fun uploadMaven(project: Project, files: List, config: BintrayConfig?) : TaskResult { - if (! packageExists(project.name)) { - throw KobaltException("Couldn't find a package called ${project.name} on bintray, please create one first" + - " as explained at https://bintray.com/docs/usermanual/uploads/uploads_creatinganewpackage.html") - } + private val service: Api - val fileToPath: (File) -> String = { f: File -> - arrayListOf( - UnauthenticatedBintrayApi.BINTRAY_URL_API_CONTENT, - org ?: username!!, - "maven", - project.name, - project.version!!, - project.group!!.replace(".", "/"), - project.artifactId!!, - project.version!!, - f.name) - .joinToString("/") - } + init { + val builder = OkHttpClient.Builder() + builder.interceptors().add(Interceptor { chain -> + var original = chain.request(); - return upload(files, config, fileToPath, generateMd5 = true) + chain.proceed(original.newBuilder() + .header("Authorization", Credentials.basic(username, password)) + .method(original.method(), original.body()) + .build()); + }) + val okHttpClient = builder.build() + + service = Retrofit.Builder() + .client(okHttpClient) + .baseUrl(BintrayApi.BINTRAY_URL_API) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(Api::class.java) } - fun uploadFile(file: File, url: String, config: BintrayConfig?, generateMd5: Boolean = false) = - upload(arrayListOf(file), config, { - f: File -> "${UnauthenticatedBintrayApi.BINTRAY_URL_API_CONTENT}/${org ?: username}/generic/$url"}, - generateMd5) + fun validatePackage(project: Project) { + val execute = service.getPackage(org ?: username!!, project.name).execute() - private fun upload(files: List, config: BintrayConfig?, fileToPath: (File) -> String, - generateMd5: Boolean = false) : TaskResult { + if (execute.errorBody()?.string()?.contains("'${project.name}' was not found") ?: false) { + warn("Package does not exist on bintray. Creating now.") + val result = service.createPackage(org ?: username!!, buildPackageInfo(project)) + .execute() + if (result.errorBody() != null) { + throw KobaltException("Error while creating package:\n" + result.errorBody().string()) + } + } + } + + private fun buildPackageInfo(project: Project): JsonObject { + val jsonObject = JsonObject() + jsonObject.addNonNull("name", project.name) + jsonObject.addNonNull("desc", project.description) + jsonObject.addNonNull("vcs_url", project.scm?.url) + jsonObject.addNonNull("website_url", project.url) + val licenses = JsonArray() + project.licenses.forEach { + licenses.add(it.name) + } + jsonObject.add("licenses", licenses) + return jsonObject + } + + fun uploadMaven(project: Project, files: List, config: BintrayConfig?): TaskResult { + validatePackage(project) + return upload(project, files, config, generateMd5 = true) + } + + fun uploadFile(project: Project, file: File, config: BintrayConfig?, generateMd5: Boolean = false) = + upload(project, arrayListOf(file), config, generateMd5) + + private fun upload(project: Project, files: List, config: BintrayConfig?, generateMd5: Boolean = false): TaskResult { val filesToUpload = arrayListOf() if (config != null && config.sign) { @@ -113,9 +153,9 @@ class BintrayApi @Inject constructor ( filesToUpload.add(it) if (generateMd5) { // Create and upload the md5 for this file - with(File(it.absolutePath)) { + with(it) { val md5: String = Md5.toMd5(this) - val md5File = File(absolutePath + ".md5") + val md5File = File(path + ".md5") md5File.writeText(md5) filesToUpload.add(md5File) } @@ -133,36 +173,43 @@ class BintrayApi @Inject constructor ( optionPath.append("?" + options.joinToString("&")) } - // - // Uploads can'be done in parallel or Bintray rejects them - // val fileCount = filesToUpload.size if (fileCount > 0) { log(1, " Found $fileCount artifacts to upload: " + filesToUpload[0] + if (fileCount > 1) "..." else "") - var i = 1 val errorMessages = arrayListOf() - - fun dots(total: Int, list: List) : String { - val spaces : String = Array(total - list.size, { " " }).joinToString("") - return "|" + list.map { if (it) "." else "X" }.joinToString("") + spaces + "|" + fun dots(total: Int, list: List, file: File?): String { + val spaces: String = Array(total - list.size, { " " }).joinToString("") + return "|" + list.map { if (it) "." else "X" }.joinToString("") + spaces + (if(file != null) "| [ ${file} ]" else "|") } val results = arrayListOf() - filesToUpload.forEach { file -> - http.uploadFile(username, password, fileToPath(file) + optionPath, - Http.TypedFile(MediaType.ANY_APPLICATION_TYPE.toString(), file), - post = false, // Bintray requires PUT - success = { r: Response -> results.add(true) }, - error = { r: Response -> - results.add(false) - val jcResponse = parseResponse(r) - errorMessages.add(jcResponse.errorMessage!!) - }) - val end = if (i >= fileCount) "\n" else "" - log(1, " Uploading " + (i++) + " / $fileCount " + dots(fileCount, results) + end, false) + filesToUpload.forEachIndexed { i, file -> + val type = MediaType.parse("multipart/form-data") + + val body = MultipartBody.Part.createFormData("artifact", file.name, RequestBody.create(type, file)); + + var upload = if(file.extension != "pom" ) { + service.uploadArtifact(org ?: username!!, project.name, + project.group!!.replace('.', '/'), project.artifactId!!, project.version!!, file.name, body) + } else { + service.uploadPom(org ?: username!!, project.name, + project.group!!.replace('.', '/'), project.artifactId!!, project.version!!, file.name, body) + } + + val result = upload.execute() + val error = result.errorBody()?.string() + if (result.errorBody() != null) { + errorMessages.add(error!!) + results.add(false) + } else { + results.add(true) + } + log(1, " Uploading ${i + 1} / $fileCount " + dots(fileCount, results, file), false) } + log(1, " Uploading ${fileCount} / $fileCount " + dots(fileCount, results, null), false) + log(1, "", true) if (errorMessages.isEmpty()) { return TaskResult() } else { @@ -174,4 +221,11 @@ class BintrayApi @Inject constructor ( return TaskResult() } } + + fun JsonObject.addNonNull(name: String, value: String?) { + if (value != null) { + addProperty(name, value); + } + } + } diff --git a/src/main/kotlin/com/beust/kobalt/plugin/publish/PublishPlugin.kt b/src/main/kotlin/com/beust/kobalt/plugin/publish/PublishPlugin.kt index db7c9e45..c94768cd 100644 --- a/src/main/kotlin/com/beust/kobalt/plugin/publish/PublishPlugin.kt +++ b/src/main/kotlin/com/beust/kobalt/plugin/publish/PublishPlugin.kt @@ -85,8 +85,7 @@ public class PublishPlugin @Inject constructor(val files: KFiles, val factory: P // Upload individual files, if applicable // configuration.files.forEach { - val taskResult = jcenter.uploadFile(File(project.directory, it.first), it.second /* url */, - configuration) + val taskResult = jcenter.uploadFile(project, File(project.directory, it.first), configuration) success = success and taskResult.success if (!taskResult.success) { messages.add(taskResult.errorMessage!!)