wanghongkai před 5 roky
revize
1a6de4867c
46 změnil soubory, kde provedl 2367 přidání a 0 odebrání
  1. 34 0
      .gitignore
  2. 13 0
      Dockerfile
  3. 89 0
      pom.xml
  4. 30 0
      seecoder-build-client/pom.xml
  5. 31 0
      seecoder-build-client/src/main/java/cn/seecoder/build/client/SeecoderBuildApi.java
  6. 277 0
      seecoder-build-client/src/main/java/cn/seecoder/build/client/SeecoderBuildClient.java
  7. 34 0
      seecoder-build-client/src/main/java/cn/seecoder/build/client/SeecoderBuildException.java
  8. 108 0
      seecoder-build-client/src/test/java/cn/seecoder/client/SeecoderBuildClientTest.java
  9. 121 0
      seecoder-build-client/src/test/resources/config.xml
  10. 38 0
      seecoder-build-common/pom.xml
  11. 28 0
      seecoder-build-common/src/main/java/cn/seecoder/build/model/vo/BuildDetailsVO.java
  12. 36 0
      seecoder-build-common/src/main/java/cn/seecoder/build/model/vo/ParamVO.java
  13. 38 0
      seecoder-build-common/src/main/java/cn/seecoder/build/model/vo/PipelineVO.java
  14. 117 0
      seecoder-build-server/pom.xml
  15. 17 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/Application.java
  16. 9 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/data/dao/PipelineDAO.java
  17. 51 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/data/entity/Pipeline.java
  18. 22 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/service/JobService.java
  19. 17 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/service/PipelineService.java
  20. 26 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/service/facade/jenkins/JenkinsApi.java
  21. 140 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/service/facade/jenkins/impl/JenkinsApiImpl.java
  22. 38 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/service/facade/jenkins/impl/PoolingJenkinsHttpClient.java
  23. 105 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/service/impl/JobServiceImpl.java
  24. 85 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/service/impl/PipelineServiceImpl.java
  25. 39 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/service/model/converter/PipelineConverter.java
  26. 30 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/util/ApplicationProperties.java
  27. 26 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/util/AsyncConfig.java
  28. 34 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/util/AsyncWrapper.java
  29. 91 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/util/JwtTokenUtil.java
  30. 36 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/util/LoggerUtil.java
  31. 27 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/util/ServiceException.java
  32. 25 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/util/XmlUtil.java
  33. 34 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/web/CustomerExceptionHandler.java
  34. 49 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/web/WebSecurityConfig.java
  35. 57 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/web/controller/api/JobController.java
  36. 47 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/web/controller/api/PipelineController.java
  37. 38 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/web/model/Response.java
  38. 29 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/AuthController.java
  39. 39 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/JwtAuthService.java
  40. 70 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/JwtAuthenticationTokenFilter.java
  41. 18 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/Sha256HexPasswordEncoder.java
  42. 75 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/User.java
  43. 36 0
      seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/UserDetailServiceImpl.java
  44. 30 0
      seecoder-build-server/src/main/resources/application.yml
  45. 20 0
      seecoder-build-server/src/test/java/cn/seecoder/build/server/AppTest.java
  46. 13 0
      settings.xml

+ 34 - 0
.gitignore

@@ -0,0 +1,34 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+*.log

+ 13 - 0
Dockerfile

@@ -0,0 +1,13 @@
+FROM maven:3.6.1 AS compile_stage
+ENV PROJECT_NAME app
+ENV WORK_PATH /opt/$PROJECT_NAME
+COPY settings.xml /root/.m2/settings.xml
+COPY . $WORK_PATH
+RUN cd $WORK_PATH && mvn clean install -DskipTests -DfinalName=$PROJECT_NAME
+
+FROM java:8-jre-alpine
+ENV PROJECT_NAME app
+ENV WORK_PATH /opt/$PROJECT_NAME
+WORKDIR /app
+COPY --from=compile_stage $WORK_PATH/seecoder-build-server/target/${PROJECT_NAME}.jar .
+CMD ["sh", "-c", "java -Xdebug -Xrunjdwp:transport=dt_socket,address=8888,server=y,suspend=n -jar -server /app/${PROJECT_NAME}.jar"]

+ 89 - 0
pom.xml

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.3.5.RELEASE</version>
+        <relativePath />
+    </parent>
+    <groupId>cn.seecoder</groupId>
+    <artifactId>seecoder-build</artifactId>
+    <packaging>pom</packaging>
+    <version>0.1.0</version>
+    <modules>
+        <module>seecoder-build-client</module>
+        <module>seecoder-build-server</module>
+        <module>seecoder-build-common</module>
+    </modules>
+    <properties>
+        <seecoder-build-version>0.1.0</seecoder-build-version>
+    </properties>
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-validation</artifactId>
+                <version>2.3.4.RELEASE</version>
+            </dependency>
+            <dependency>
+                <groupId>io.jsonwebtoken</groupId>
+                <artifactId>jjwt</artifactId>
+                <version>0.9.0</version>
+            </dependency>
+            <!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
+            <dependency>
+                <groupId>commons-codec</groupId>
+                <artifactId>commons-codec</artifactId>
+                <version>1.15</version>
+            </dependency>
+            <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
+            <dependency>
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-lang3</artifactId>
+                <version>3.11</version>
+            </dependency>
+            <dependency>
+                <groupId>junit</groupId>
+                <artifactId>junit</artifactId>
+                <version>4.11</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>com.offbytwo.jenkins</groupId>
+                <artifactId>jenkins-client</artifactId>
+                <version>0.3.8</version>
+            </dependency>
+            <!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
+            <dependency>
+                <groupId>com.squareup.okhttp3</groupId>
+                <artifactId>okhttp</artifactId>
+                <version>4.9.0</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.seecoder</groupId>
+                <artifactId>seecoder-build-common</artifactId>
+                <version>${seecoder-build-version}</version>
+            </dependency>
+            <dependency>
+                <groupId>cn.seecoder</groupId>
+                <artifactId>seecoder-build-server</artifactId>
+                <version>${seecoder-build-version}</version>
+            </dependency>
+            <dependency>
+                <groupId>cn.seecoder</groupId>
+                <artifactId>seecoder-build-client</artifactId>
+                <version>${seecoder-build-version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+    <distributionManagement>
+        <repository>
+            <id>nexus</id>
+            <url>http://nexus.seecoder.cn/repository/maven-releases/</url>
+        </repository>
+    </distributionManagement>
+</project>

+ 30 - 0
seecoder-build-client/pom.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>seecoder-build</artifactId>
+        <groupId>cn.seecoder</groupId>
+        <version>0.1.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>seecoder-build-client</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>cn.seecoder</groupId>
+            <artifactId>seecoder-build-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 31 - 0
seecoder-build-client/src/main/java/cn/seecoder/build/client/SeecoderBuildApi.java

@@ -0,0 +1,31 @@
+package cn.seecoder.build.client;
+
+import cn.seecoder.build.model.vo.BuildDetailsVO;
+import cn.seecoder.build.model.vo.PipelineVO;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+public interface SeecoderBuildApi {
+
+    PipelineVO createOrUpdatePipeline(PipelineVO vo, boolean create) throws SeecoderBuildException;
+
+    void delete(Integer id) throws SeecoderBuildException;
+
+    List<PipelineVO> getAll() throws SeecoderBuildException;
+
+    PipelineVO getDetailById(Integer id) throws SeecoderBuildException;
+
+    String createJob(Integer pipelineId, String jobName) throws SeecoderBuildException;
+
+    void deleteJob(String jobName) throws SeecoderBuildException;
+
+    BuildDetailsVO getBuildDetails(String jobName, Integer buildId) throws SeecoderBuildException;
+
+    String getEntity(String jobName, Integer buildId, String path) throws SeecoderBuildException;
+
+    InputStream getArtifact(String jobName, Integer buildId, String path) throws SeecoderBuildException;
+
+    void invokeBuild(Integer pipelineId, String jobName, Map<String,String> params) throws SeecoderBuildException;
+}

+ 277 - 0
seecoder-build-client/src/main/java/cn/seecoder/build/client/SeecoderBuildClient.java

@@ -0,0 +1,277 @@
+package cn.seecoder.build.client;
+
+import cn.seecoder.build.model.vo.BuildDetailsVO;
+import cn.seecoder.build.model.vo.PipelineVO;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import okhttp3.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static cn.seecoder.build.client.SeecoderBuildException.BIZ_ERROR_CODE;
+
+public class SeecoderBuildClient implements SeecoderBuildApi {
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    private OkHttpClient client;
+
+    private String token;
+
+    private String host;
+
+    public SeecoderBuildClient(String host, String token) {
+        this.token = token;
+        this.host = host;
+        client = new OkHttpClient.Builder()
+                .connectTimeout(60, TimeUnit.SECONDS)
+                .writeTimeout(60,TimeUnit.SECONDS)
+                .retryOnConnectionFailure(true)
+                .build();
+    }
+
+    private <T> T processResponse(ObjectMapper objectMapper, Response response, Class<T> clazz) {
+        try {
+            validateResponseStatus(response);
+            String body = response.body().string();
+            JsonNode node = objectMapper.readTree(body);
+            Integer code = Integer.parseInt(node.get("code").asText());
+            String msg = node.get("msg").asText();
+            if (code != 0) {
+                throw new SeecoderBuildException(BIZ_ERROR_CODE, msg);
+            }
+            if (clazz == null) {
+                return null;
+            }
+            return objectMapper.treeToValue(node.get("result"), clazz);
+        } catch (IOException e) {
+            logger.error("IOException", e);
+            throw SeecoderBuildException.IO_ERROR;
+        }
+    }
+
+    private void validateResponseStatus(Response response) {
+        if (response.code() == 403) {
+            throw SeecoderBuildException.UNAHTORIZATION_ERROR;
+        } else if (response.code() == 500) {
+            throw SeecoderBuildException.INTERNAL_SERVER_ERROR;
+        } else if (response.code() != 200) {
+            throw SeecoderBuildException.UNKNOWN_ERROR;
+        }
+    }
+
+
+    @Override
+    public PipelineVO createOrUpdatePipeline(PipelineVO vo, boolean create) throws SeecoderBuildException {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            Request.Builder builder = new Request.Builder()
+                    .addHeader("Authorization", token);
+            if (create) {
+                builder = builder
+                        .url(host + "/api/pipeline")
+                        .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), objectMapper.writeValueAsString(vo)));
+            } else {
+                builder = builder
+                        .url(host + "/api/pipeline/" + vo.getId())
+                        .put(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), objectMapper.writeValueAsString(vo)));
+            }
+            Request request = builder.build();
+            final Call call = client.newCall(request);
+            Response response = call.execute();
+            PipelineVO pipelineVO = processResponse(objectMapper, response, PipelineVO.class);
+            return pipelineVO;
+        } catch (JsonProcessingException e) {
+            logger.error("JSON writeValueAsString error", e);
+            throw SeecoderBuildException.JSON_ERROR;
+        } catch (IOException e) {
+            logger.error("IOException", e);
+            throw SeecoderBuildException.IO_ERROR;
+        }
+    }
+
+    @Override
+    public void delete(Integer id) {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            Request request = new Request.Builder()
+                    .url(host + "/api/pipeline/" + id)
+                    .delete()
+                    .addHeader("Authorization", token)
+                    .build();
+            final Call call = client.newCall(request);
+            Response response = call.execute();
+            processResponse(objectMapper, response, null);
+        } catch (IOException e) {
+            logger.error("IOException", e);
+            throw SeecoderBuildException.IO_ERROR;
+        }
+    }
+
+    @Override
+    public List<PipelineVO> getAll() {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            Request request = new Request.Builder()
+                    .url(host + "/api/pipeline")
+                    .get()
+                    .addHeader("Authorization", token)
+                    .build();
+            final Call call = client.newCall(request);
+            Response response = call.execute();
+            PipelineVO[] pipelines = processResponse(objectMapper, response, PipelineVO[].class);
+            return Arrays.asList(pipelines);
+        } catch (IOException e) {
+            logger.error("IOException", e);
+            throw SeecoderBuildException.IO_ERROR;
+        }
+    }
+
+    @Override
+    public PipelineVO getDetailById(Integer id) {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            Request request = new Request.Builder()
+                    .url(host + "/api/pipeline/" + id)
+                    .get()
+                    .addHeader("Authorization", token)
+                    .build();
+            final Call call = client.newCall(request);
+            Response response = call.execute();
+            return processResponse(objectMapper, response, PipelineVO.class);
+        } catch (IOException e) {
+            logger.error("IOException", e);
+            throw SeecoderBuildException.IO_ERROR;
+        }
+    }
+
+    @Override
+    public String createJob(Integer pipelineId, String jobName) throws SeecoderBuildException {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            Map<String, Object> params = new HashMap<>();
+            params.put("pipelineId", pipelineId);
+            if (jobName != null) {
+                params.put("jobName", jobName);
+            }
+            Request request = new Request.Builder()
+                    .url(host + "/api/job")
+                    .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), objectMapper.writeValueAsString(params)))
+                    .addHeader("Authorization", token)
+                    .build();
+            final Call call = client.newCall(request);
+            Response response = call.execute();
+            return processResponse(objectMapper, response, String.class);
+        } catch (JsonProcessingException e) {
+            logger.error("JSON writeValueAsString error", e);
+            throw SeecoderBuildException.JSON_ERROR;
+        } catch (IOException e) {
+            logger.error("IOException", e);
+            throw SeecoderBuildException.IO_ERROR;
+        }
+    }
+
+    @Override
+    public void deleteJob(String jobName) throws SeecoderBuildException {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            Request request = new Request.Builder()
+                    .url(host + "/api/job/" + jobName)
+                    .delete()
+                    .addHeader("Authorization", token)
+                    .build();
+            final Call call = client.newCall(request);
+            Response response = call.execute();
+            processResponse(objectMapper, response, null);
+        } catch (IOException e) {
+            logger.error("IOException", e);
+            throw SeecoderBuildException.IO_ERROR;
+        }
+    }
+
+    @Override
+    public BuildDetailsVO getBuildDetails(String jobName, Integer buildId) throws SeecoderBuildException {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            Request request = new Request.Builder()
+                    .url(host + "/api/job/" + jobName + "/build/" + buildId)
+                    .get()
+                    .addHeader("Authorization", token)
+                    .build();
+            final Call call = client.newCall(request);
+            Response response = call.execute();
+            return processResponse(objectMapper, response, BuildDetailsVO.class);
+        } catch (IOException e) {
+            logger.error("IOException", e);
+            throw SeecoderBuildException.IO_ERROR;
+        }
+    }
+
+    @Override
+    public String getEntity(String jobName, Integer buildId, String path) throws SeecoderBuildException {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            Request request = new Request.Builder()
+                    .url(host + "/api/job/entity?jobName=" + jobName + "&buildId=" + buildId + "&path=" +path)
+                    .get()
+                    .addHeader("Authorization", token)
+                    .build();
+            final Call call = client.newCall(request);
+            Response response = call.execute();
+            return processResponse(objectMapper, response, String.class);
+        } catch (IOException e) {
+            logger.error("IOException", e);
+            throw SeecoderBuildException.IO_ERROR;
+        }
+    }
+
+    @Override
+    public InputStream getArtifact(String jobName, Integer buildId, String path) throws SeecoderBuildException {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            Request request = new Request.Builder()
+                    .url(host + "/api/job/artifact?jobName=" + jobName + "&buildId=" + buildId + "&path=" +path)
+                    .get()
+                    .addHeader("Authorization", token)
+                    .build();
+            final Call call = client.newCall(request);
+            Response response = call.execute();
+            validateResponseStatus(response);
+            return response.body().byteStream();
+        } catch (IOException e) {
+            logger.error("IOException", e);
+            throw SeecoderBuildException.IO_ERROR;
+        }
+    }
+
+    @Override
+    public void invokeBuild(Integer pipelineId, String jobName, Map<String, String> params) throws SeecoderBuildException {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            Map<String, Object> paramsMap = new HashMap<>();
+            paramsMap.put("pipelineId", pipelineId);
+            paramsMap.put("jobName", jobName);
+            paramsMap.put("params", params);
+            Request request = new Request.Builder()
+                    .url(host + "/api/job/run")
+                    .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), objectMapper.writeValueAsString(paramsMap)))
+                    .addHeader("Authorization", token)
+                    .build();
+            final Call call = client.newCall(request);
+            Response response = call.execute();
+            processResponse(objectMapper, response, null);
+        } catch (IOException e) {
+            logger.error("IOException", e);
+            throw SeecoderBuildException.IO_ERROR;
+        }
+    }
+}

+ 34 - 0
seecoder-build-client/src/main/java/cn/seecoder/build/client/SeecoderBuildException.java

@@ -0,0 +1,34 @@
+package cn.seecoder.build.client;
+
+public class SeecoderBuildException extends RuntimeException {
+
+    public static final SeecoderBuildException UNKNOWN_ERROR = new SeecoderBuildException("-1", "Unknown Error");
+
+    public static final SeecoderBuildException JSON_ERROR = new SeecoderBuildException("101", "JSON Processing Error");
+
+    public static final SeecoderBuildException IO_ERROR = new SeecoderBuildException("102", "Execute IO Error");
+
+    public static final SeecoderBuildException UNAHTORIZATION_ERROR = new SeecoderBuildException("403", "Unauthorization Error");
+
+    public static final SeecoderBuildException NOT_FOUND_ERROR = new SeecoderBuildException("404", "Not Found Error");
+
+    public static final SeecoderBuildException INTERNAL_SERVER_ERROR = new SeecoderBuildException("500", "Internal Server Error");
+
+    public static final String BIZ_ERROR_CODE = "100";
+
+    private String code;
+
+    private String msg;
+
+    public SeecoderBuildException(String code, String msg) {
+        super(msg);
+        this.code = code;
+        this.msg = msg;
+    }
+
+    public SeecoderBuildException(String code, String msg, Throwable cause) {
+        super(msg, cause);
+        this.code = code;
+        this.msg = msg;
+    }
+}

+ 108 - 0
seecoder-build-client/src/test/java/cn/seecoder/client/SeecoderBuildClientTest.java

@@ -0,0 +1,108 @@
+package cn.seecoder.client;
+
+import cn.seecoder.build.client.SeecoderBuildClient;
+import cn.seecoder.build.model.vo.BuildDetailsVO;
+import cn.seecoder.build.model.vo.PipelineVO;
+import org.apache.commons.io.IOUtils;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class SeecoderBuildClientTest {
+
+    private SeecoderBuildClient client;
+
+    private String template;
+
+    @Before
+    public void init() throws URISyntaxException, IOException {
+        client = new SeecoderBuildClient("http://localhost:8080", "3b861b052fd085887b5462fe5ae0c3a916b2c281d776a4d87e7bd5572ad24608");
+        template = IOUtils.readLines(getClass().getClassLoader().getResourceAsStream("config.xml")).stream().collect(Collectors.joining("\n"));
+    }
+
+    @Test
+    public void testCreatePipeline() {
+        PipelineVO pipelineVO = new PipelineVO();
+        pipelineVO.setName("test");
+        pipelineVO.setScript(template);
+        PipelineVO vo = client.createOrUpdatePipeline(pipelineVO, true);
+        Assert.assertTrue(vo != null);
+    }
+
+    @Test
+    public void testUpdatePipeline() {
+        PipelineVO pipelineVO = new PipelineVO();
+        pipelineVO.setName("test4");
+        pipelineVO.setScript(template);
+        PipelineVO vo = client.createOrUpdatePipeline(pipelineVO, true);
+        Assert.assertTrue(vo != null);
+        vo.setName("test4u");
+        vo.setDescription("test");
+        vo = client.createOrUpdatePipeline(vo, false);
+        Assert.assertTrue(vo != null);
+    }
+
+    @Test
+    public void testDeletePipeline() {
+        PipelineVO pipelineVO = new PipelineVO();
+        pipelineVO.setName("test4");
+        pipelineVO.setScript(template);
+        PipelineVO vo = client.createOrUpdatePipeline(pipelineVO, true);
+        client.delete(vo.getId());
+    }
+
+    @Test
+    public void testGetAll() {
+        List<PipelineVO> vos = client.getAll();
+        vos.stream().forEach(System.out::println);
+    }
+    @Test
+    public void testGetDetail() {
+        PipelineVO vo = client.getDetailById(2);
+        System.out.println(vo);
+    }
+    @Test
+    public void createJob() {
+        String jobName = client.createJob(2, null);
+        System.out.println(jobName);
+    }
+    @Test
+    public void deleteJob() {
+        String jobName = client.createJob(2, null);
+        System.out.println(jobName);
+        client.deleteJob(jobName);
+    }
+    @Test
+    public void getBuildDetails() {
+        BuildDetailsVO detailsVO = client.getBuildDetails("Test", 15);
+        System.out.println(detailsVO);
+    }
+    @Test
+    public void getArtifact() {
+        InputStream inputStream = client.getArtifact("Test", 16, "target/myapp-1.0.jar");
+    }
+    @Test
+    public void invokeBuild() {
+        Map<String,String> params = new HashMap<>();
+        params.put("GIT_URL", "http://gogs.seec.seecoder.cn/WangHongKai/Test");
+        params.put("COMMIT_HASH", "master");
+        params.put("DOCKER_IMAGE", "registry.cn-hangzhou.aliyuncs.com/acs/maven:3-jdk-8");
+        params.put("EXECUTE_SHELL", "mvn cobertura:cobertura");
+        params.put("PROB_TARGET", "COVERAGE");
+        params.put("JAR_ARTIFACT", "org.jacoco.examples.maven.java-1.0-SNAPSHOT.jar");
+        client.invokeBuild(2, "Test", params);
+    }
+    @Test
+    public void getEntity() {
+        String entity = client.getEntity("Test", 26, "/cobertura?depth=2");
+        System.out.println(entity);
+    }
+}

+ 121 - 0
seecoder-build-client/src/test/resources/config.xml

@@ -0,0 +1,121 @@
+<?xml version='1.1' encoding='UTF-8'?>
+<flow-definition plugin="workflow-job@2.21">
+    <description/>
+    <keepDependencies>false</keepDependencies>
+    <properties>
+        <hudson.model.ParametersDefinitionProperty>
+            <parameterDefinitions>
+                <hudson.model.TextParameterDefinition>
+                    <name>PROB_TARGET</name>
+                    <description/>
+                    <defaultValue/>
+                    <trim>false</trim>
+                </hudson.model.TextParameterDefinition>
+                <hudson.model.TextParameterDefinition>
+                    <name>GIT_URL</name>
+                    <description/>
+                    <defaultValue/>
+                    <trim>false</trim>
+                </hudson.model.TextParameterDefinition>
+                <hudson.model.TextParameterDefinition>
+                    <name>COMMIT_HASH</name>
+                    <description/>
+                    <defaultValue/>
+                    <trim>false</trim>
+                </hudson.model.TextParameterDefinition>
+                <hudson.model.TextParameterDefinition>
+                    <name>DOCKER_IMAGE</name>
+                    <description/>
+                    <defaultValue/>
+                    <trim>false</trim>
+                </hudson.model.TextParameterDefinition>
+                <hudson.model.TextParameterDefinition>
+                    <name>EXECUTE_SHELL</name>
+                    <description/>
+                    <defaultValue/>
+                    <trim>false</trim>
+                </hudson.model.TextParameterDefinition>
+                <hudson.model.TextParameterDefinition>
+                    <name>TARGET_JAR</name>
+                    <description/>
+                    <defaultValue/>
+                    <trim>false</trim>
+                </hudson.model.TextParameterDefinition>
+            </parameterDefinitions>
+        </hudson.model.ParametersDefinitionProperty>
+    </properties>
+    <definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition" plugin="workflow-cps@2.53">
+        <script>
+            pipeline {
+            agent any
+            stages {
+            stage(&apos;Clean Workspace&apos;) {
+            steps {
+            deleteDir()
+            }
+            }
+            stage(&apos;Pull Code&apos;) {
+            steps {
+            git credentialsId: &quot;MOOCODER&quot;, url: &quot;${GIT_URL}&quot;
+            }
+            }
+            stage(&apos;Reset Git HEAD&apos;) {
+            steps {
+            sh &quot;git reset --hard ${COMMIT_HASH}&quot;
+            }
+            }
+            stage(&apos;Run Build&apos;) {
+            agent {
+            docker {
+            image &quot;${DOCKER_IMAGE}&quot;
+            args &apos;--cpus 1 -m 1G&apos;<!-- mount type=tmpfs,destination=/var/ws/tmp,tmpfs-size=134217728-->
+            reuseNode true
+            }
+            }
+            steps {
+                echo &apos;==CONSOLE OUTPUT BEGIN==&apos;
+                catchError {
+                    sh &quot;${EXECUTE_SHELL}&quot;
+                }
+                archiveArtifacts allowEmptyArchive: true, artifacts: &apos;${TARGET_JAR}&apos;
+                echo &apos;==CONSOLE OUTPUT END==&apos;
+            }
+            }
+            stage(&apos;Collect DLTest Report&apos;) {
+            when {
+            environment name: &apos;PROB_TARGET&apos;, value: &apos;TEST&apos;
+            }
+            steps {
+            junit allowEmptyResults: true, testResults: &apos;test-reports/*.xml&apos;
+            }
+            }
+            stage(&apos;Collect Coverage Report&apos;) {
+            when {
+            environment name: &apos;PROB_TARGET&apos;, value: &apos;COVERAGE&apos;
+            }
+            steps {
+            cobertura autoUpdateHealth: false, autoUpdateStability: false, coberturaReportFile: &apos;coverage-reports/*.xml&apos;, conditionalCoverageTargets: &apos;70, 0, 0&apos;, failNoReports: false, failUnhealthy: false, failUnstable: false, lineCoverageTargets: &apos;80, 0, 0&apos;, maxNumberOfBuilds: 0, methodCoverageTargets: &apos;80, 0, 0&apos;, onlyStable: false, sourceEncoding: &apos;ASCII&apos;, zoomCoverageChart: false
+            }
+            }
+            stage(&apos;Collect Mutation Report&apos;) {
+            when {
+            environment name: &apos;PROB_TARGET&apos;, value: &apos;MUTATION&apos;
+            }
+            steps {
+            archiveArtifacts allowEmptyArchive: true, artifacts: &apos;mutation-reports/mutations.xml&apos;
+            }
+            }
+            }
+            post {
+            always {
+            cleanWs()
+            sh &apos;curl -o /dev/null -s -X POST -H &quot;X-Moocoder-Secret: ${MOOCODER_SECRET}&quot; &quot;${MOOCODER_HOST}/internal/notify/callback/${JOB_NAME}/${BUILD_NUMBER}&quot; || :&apos;
+            }
+            }
+            }
+        </script>
+        <sandbox>true</sandbox>
+    </definition>
+    <triggers/>
+    <disabled>false</disabled>
+</flow-definition>

+ 38 - 0
seecoder-build-common/pom.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>seecoder-build</artifactId>
+        <groupId>cn.seecoder</groupId>
+        <version>0.1.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>seecoder-build-common</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.offbytwo.jenkins</groupId>
+            <artifactId>jenkins-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>
+    </dependencies>
+
+
+</project>

+ 28 - 0
seecoder-build-common/src/main/java/cn/seecoder/build/model/vo/BuildDetailsVO.java

@@ -0,0 +1,28 @@
+package cn.seecoder.build.model.vo;
+
+
+import com.offbytwo.jenkins.model.Artifact;
+import com.offbytwo.jenkins.model.BuildResult;
+import lombok.Data;
+import lombok.ToString;
+
+import java.util.List;
+
+@Data
+@ToString
+public class BuildDetailsVO {
+
+    private String jobName;
+
+    private Integer buildId;
+
+    private Long duration;
+
+    private String consoleOutput;
+
+    private BuildResult result;
+
+    private List<Artifact> artifacts;
+
+    private Long timestamps;
+}

+ 36 - 0
seecoder-build-common/src/main/java/cn/seecoder/build/model/vo/ParamVO.java

@@ -0,0 +1,36 @@
+package cn.seecoder.build.model.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.w3c.dom.Element;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
+public class ParamVO {
+
+    @JsonProperty("name")
+    private String name;
+
+    @JsonProperty("description")
+    private String description;
+
+    @JsonProperty("defaultValue")
+    private String defaultValue;
+
+    @JsonProperty("trim")
+    private Boolean trim;
+
+    public ParamVO(Element node) {
+        if (node != null) {
+            this.name = node.getElementsByTagName("name").item(0) != null ? node.getElementsByTagName("name").item(0).getTextContent() : null;
+            this.description = node.getElementsByTagName("description").item(0) != null ? node.getElementsByTagName("description").item(0).getTextContent() : null;
+            this.defaultValue = node.getElementsByTagName("defaultValue").item(0) != null ? node.getElementsByTagName("defaultValue").item(0).getTextContent() : null;
+            this.trim = node.getElementsByTagName("trim").item(0) != null ? Boolean.valueOf(node.getElementsByTagName("trim").item(0).getTextContent()) : null;
+        }
+    }
+}

+ 38 - 0
seecoder-build-common/src/main/java/cn/seecoder/build/model/vo/PipelineVO.java

@@ -0,0 +1,38 @@
+package cn.seecoder.build.model.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import lombok.Data;
+import lombok.ToString;
+
+import javax.validation.constraints.NotBlank;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@ToString
+public class PipelineVO {
+
+    private Integer id;
+
+    @NotBlank
+    private String name;
+
+    private String description;
+
+    private String script;
+
+    private List<ParamVO> params = new ArrayList<>();
+
+    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
+    @JsonSerialize(using = LocalDateTimeSerializer.class)
+    private LocalDateTime createdAt;
+
+    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
+    @JsonSerialize(using = LocalDateTimeSerializer.class)
+    private LocalDateTime modifiedAt;
+
+}

+ 117 - 0
seecoder-build-server/pom.xml

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>seecoder-build-server</artifactId>
+    <name>seecoder-build-server</name>
+
+    <packaging>jar</packaging>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <java.version>1.8</java.version>
+        <finalName>${project.artifactId}-${project.version}</finalName>
+    </properties>
+
+    <parent>
+        <groupId>cn.seecoder</groupId>
+        <artifactId>seecoder-build</artifactId>
+        <version>0.1.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.junit.vintage</groupId>
+                    <artifactId>junit-vintage-engine</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>1.15</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.offbytwo.jenkins</groupId>
+            <artifactId>jenkins-client</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.seecoder</groupId>
+            <artifactId>seecoder-build-common</artifactId>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 17 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/Application.java

@@ -0,0 +1,17 @@
+package cn.seecoder.build.server;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+@SpringBootApplication
+@EnableJpaRepositories(basePackages = "cn.seecoder.build.server.data")
+@EnableJpaAuditing
+public class Application {
+
+    public static void main(String[] args) {
+        SpringApplication.run(Application.class, args);
+    }
+
+}

+ 9 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/data/dao/PipelineDAO.java

@@ -0,0 +1,9 @@
+package cn.seecoder.build.server.data.dao;
+
+import cn.seecoder.build.server.data.entity.Pipeline;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface PipelineDAO extends JpaRepository<Pipeline, Integer> {
+}

+ 51 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/data/entity/Pipeline.java

@@ -0,0 +1,51 @@
+package cn.seecoder.build.server.data.entity;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.apache.tomcat.jni.Local;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+
+@Data
+@ToString
+@EqualsAndHashCode
+@Entity
+@Table(name = "pipeline")
+@EntityListeners(AuditingEntityListener.class)
+public class Pipeline {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private Integer id;
+
+    @Basic
+    @Column(name = "script", nullable = false, columnDefinition = "TEXT NOT NULL")
+    private String script;
+
+    @Basic
+    @Column(name = "name" ,nullable = false)
+    private String name;
+
+    @Basic
+    @Column(name = "description")
+    private String description;
+
+    @Basic
+    @Column(name = "params", columnDefinition = "TEXT")
+    private String params;
+
+    @CreatedDate
+    @Column(name = "created_at")
+    private LocalDateTime createdAt;
+
+    @LastModifiedDate
+    @Column(name = "modifiedAt")
+    private LocalDateTime modifiedAt;
+
+}

+ 22 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/service/JobService.java

@@ -0,0 +1,22 @@
+package cn.seecoder.build.server.service;
+
+import cn.seecoder.build.model.vo.BuildDetailsVO;
+import cn.seecoder.build.server.util.ServiceException;
+
+import java.io.InputStream;
+import java.util.Map;
+
+public interface JobService {
+
+    String createJob(Integer pipelineId, String jobName) throws ServiceException;
+
+    void deleteJob(String jobName) throws ServiceException;
+
+    BuildDetailsVO getBuildDetails(String jobName, Integer buildId) throws ServiceException;
+
+    String getEntity(String jobName, Integer buildId, String path) throws ServiceException;
+
+    InputStream getArtifact(String jobName, Integer buildId, String path) throws ServiceException;
+
+    void invokeBuild(Integer pipelineId, String jobName, Map<String,String> params) throws ServiceException;
+}

+ 17 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/service/PipelineService.java

@@ -0,0 +1,17 @@
+package cn.seecoder.build.server.service;
+
+import cn.seecoder.build.model.vo.PipelineVO;
+import cn.seecoder.build.server.util.ServiceException;
+
+import java.util.List;
+
+public interface PipelineService {
+
+    PipelineVO createOrUpdate(PipelineVO vo) throws ServiceException;
+
+    void delete(Integer id);
+
+    List<PipelineVO> getAll() throws ServiceException;
+
+    PipelineVO getDetailById(Integer id) throws ServiceException;
+}

+ 26 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/service/facade/jenkins/JenkinsApi.java

@@ -0,0 +1,26 @@
+package cn.seecoder.build.server.service.facade.jenkins;
+
+import cn.seecoder.build.server.util.ServiceException;
+import com.offbytwo.jenkins.model.BaseModel;
+import com.offbytwo.jenkins.model.BuildWithDetails;
+import com.offbytwo.jenkins.model.QueueItem;
+
+import java.io.InputStream;
+import java.util.Map;
+
+public interface JenkinsApi {
+
+    void createJob(String jobName, String configTemplate) throws ServiceException;
+
+    QueueItem invokeBuild(String jobName, Map<String, String> params) throws ServiceException;
+
+    void deleteJob(String jobName) throws ServiceException;
+
+    BuildWithDetails fetchBuildDetails(String jobName, int buildNumber, boolean waitFinished) throws ServiceException;
+
+    <T extends BaseModel> T get(BuildWithDetails build, String path, Class<T> cls) throws ServiceException;
+
+    String get(BuildWithDetails build, String path) throws ServiceException;
+
+    InputStream downloadArtifact(BuildWithDetails build, String relativePath) throws ServiceException;
+}

+ 140 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/service/facade/jenkins/impl/JenkinsApiImpl.java

@@ -0,0 +1,140 @@
+package cn.seecoder.build.server.service.facade.jenkins.impl;
+
+import cn.seecoder.build.server.service.facade.jenkins.JenkinsApi;
+import cn.seecoder.build.server.util.ApplicationProperties;
+import cn.seecoder.build.server.util.LoggerUtil;
+import cn.seecoder.build.server.util.ServiceException;
+import com.offbytwo.jenkins.JenkinsServer;
+import com.offbytwo.jenkins.client.JenkinsHttpClient;
+import com.offbytwo.jenkins.model.*;
+import org.slf4j.Logger;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+
+@Service
+public class JenkinsApiImpl implements JenkinsApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(JenkinsApiImpl.class);
+
+    private final ApplicationProperties properties;
+
+    private JenkinsServer server;
+
+    public JenkinsApiImpl(ApplicationProperties properties) {
+        this.properties = properties;
+    }
+
+    @PostConstruct
+    private void initialize() throws URISyntaxException, IOException {
+        ApplicationProperties.Jenkins jenkins = properties.getJenkins();
+        URI uri = new URI(jenkins.getHost());
+        String username = jenkins.getUsername();
+        String token = jenkins.getToken();
+        JenkinsHttpClient client = new PoolingJenkinsHttpClient(uri, username, token);
+        server = new JenkinsServer(client);
+    }
+
+    @Override
+    public void createJob(String jobName, String configTemplate) throws ServiceException {
+        try {
+            server.createJob(jobName, configTemplate);
+        } catch (IOException e) {
+            logger.error("createJob exception={}, jobName={}, configTemplate={}", e, jobName, configTemplate);
+            throw new ServiceException("101", e.getMessage());
+        }
+    }
+
+    @Override
+    public QueueItem invokeBuild(String jobName, Map<String, String> params) throws ServiceException {
+        QueueReference reference;
+        try {
+            if (params == null) {
+                reference = server.getJob(jobName).build();
+            } else {
+                // 0.3.7版本的Jenkins客户端库中build(Map<String, String> params)方法存在BUG,会触发两次构建
+                reference = server.getJob(jobName).build(params, false);
+            }
+            QueueItem item = server.getQueueItem(reference);
+            while (item.getExecutable() == null) {
+                try {
+                    Thread.sleep(500);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+                item = server.getQueueItem(reference);
+            }
+            return item;
+        } catch (IOException e) {
+            logger.error("invokeBuild exception={}, jobName={}, params={}", e, jobName, params);
+            throw new ServiceException("101", e.getMessage());
+        }
+    }
+
+    @Override
+    public void deleteJob(String jobName) throws ServiceException {
+        try {
+            server.deleteJob(jobName);
+        } catch (IOException e) {
+            logger.error("deleteJob exception={}, jobName={}", e, jobName);
+            throw new ServiceException("101", e.getMessage());
+        }
+    }
+
+    @Override
+    public BuildWithDetails fetchBuildDetails(String jobName, int buildNumber, boolean waitFinished) throws ServiceException {
+        try {
+            BuildWithDetails build;
+            build = server.getJob(jobName).getBuildByNumber(buildNumber).details();
+            while (waitFinished && (build.getResult() == null || build.getDuration() == 0)) {
+                try {
+                    Thread.sleep(500);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+                build = server.getJob(jobName).getBuildByNumber(buildNumber).details();
+            }
+            return build;
+        } catch (IOException e) {
+            logger.error("deleteJob exception={}, jobName={}, buildNumber={}, waitFinished", e, jobName, buildNumber, waitFinished);
+            throw new ServiceException("101", e.getMessage());
+        }
+    }
+
+    @Override
+    public <T extends BaseModel> T get(BuildWithDetails build, String path, Class<T> cls) throws ServiceException {
+        try {
+            return  build.getClient().get(build.getUrl() + path, cls);
+        } catch (IOException e) {
+            logger.error("get exception={}, path={}", e, path);
+            throw new ServiceException("101", e.getMessage());
+        }
+    }
+
+    @Override
+    public String get(BuildWithDetails build, String path) throws ServiceException {
+        try {
+            return  build.getClient().get(build.getUrl() + path);
+        } catch (IOException e) {
+            logger.error("get exception={}, path={}", e, path);
+            throw new ServiceException("101", e.getMessage());
+        }
+    }
+
+    @Override
+    public InputStream downloadArtifact(BuildWithDetails build, String relativePath) throws ServiceException {
+        try {
+            Artifact artifact = new Artifact();
+            artifact.setRelativePath(relativePath);
+            return  build.downloadArtifact(artifact);
+        } catch (IOException | URISyntaxException e) {
+            logger.error("get exception={}, relativePath={}", e, relativePath);
+            throw new ServiceException("101", e.getMessage());
+        }
+    }
+}

+ 38 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/service/facade/jenkins/impl/PoolingJenkinsHttpClient.java

@@ -0,0 +1,38 @@
+package cn.seecoder.build.server.service.facade.jenkins.impl;
+
+import com.offbytwo.jenkins.client.JenkinsHttpClient;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.HttpContext;
+
+import java.net.URI;
+
+class PoolingJenkinsHttpClient extends JenkinsHttpClient {
+    PoolingJenkinsHttpClient(URI uri, String username, String password) {
+        super(uri, poolingHttpClient(uri, username, password));
+        if (StringUtils.isNotBlank(username)) {
+            HttpContext localContext = new BasicHttpContext();
+            localContext.setAttribute("preemptive-auth", new BasicScheme());
+            super.setLocalContext(localContext);
+        }
+    }
+
+    private static CloseableHttpClient poolingHttpClient(URI uri, String username, String password) {
+        HttpClientBuilder builder = addAuthentication(HttpClientBuilder.create(), uri, username, password);
+        PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();
+        manager.setMaxTotal(256);
+        manager.setDefaultMaxPerRoute(128);
+        builder.setDefaultRequestConfig(RequestConfig.custom()
+                .setConnectTimeout(3000)
+                .setConnectionRequestTimeout(5000)
+                .setSocketTimeout(10000)
+                .build())
+                .setConnectionManager(manager);
+        return builder.build();
+    }
+}

+ 105 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/service/impl/JobServiceImpl.java

@@ -0,0 +1,105 @@
+package cn.seecoder.build.server.service.impl;
+
+import cn.seecoder.build.model.vo.BuildDetailsVO;
+import cn.seecoder.build.model.vo.PipelineVO;
+import cn.seecoder.build.server.data.dao.PipelineDAO;
+import cn.seecoder.build.server.data.entity.Pipeline;
+import cn.seecoder.build.server.service.JobService;
+import cn.seecoder.build.server.service.facade.jenkins.JenkinsApi;
+import cn.seecoder.build.server.service.model.converter.PipelineConverter;
+import cn.seecoder.build.server.util.ServiceException;
+import com.offbytwo.jenkins.model.BuildWithDetails;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+
+@Service
+public class JobServiceImpl implements JobService {
+
+    private final JenkinsApi jenkinsApi;
+
+    private final PipelineDAO pipelineDAO;
+
+    @Autowired
+    public JobServiceImpl(JenkinsApi jenkinsApi, PipelineDAO pipelineDAO) {
+        this.jenkinsApi = jenkinsApi;
+        this.pipelineDAO = pipelineDAO;
+    }
+
+    @Override
+    public String createJob(Integer pipelineId, String jobName) throws ServiceException {
+        Pipeline pipeline = pipelineDAO.findById(pipelineId).orElse(null);
+        if (pipeline == null) {
+            throw ServiceException.BAD_REQUEST;
+        }
+        if (StringUtils.isEmpty(jobName)) {
+            jobName = RandomStringUtils.random(8, true, true);
+        }
+        jenkinsApi.createJob(jobName, pipeline.getScript());
+        return jobName;
+    }
+
+    @Override
+    public void deleteJob(String jobName) throws ServiceException {
+        jenkinsApi.deleteJob(jobName);
+    }
+
+    @Override
+    public BuildDetailsVO getBuildDetails(String jobName, Integer buildId) throws ServiceException {
+        BuildWithDetails buildWithDetails  = jenkinsApi.fetchBuildDetails(jobName, buildId, true);
+        if (buildWithDetails != null) {
+            try {
+                BuildDetailsVO buildDetailsVO = new BuildDetailsVO();
+                buildDetailsVO.setBuildId(buildId);
+                buildDetailsVO.setJobName(jobName);
+                buildDetailsVO.setConsoleOutput(buildWithDetails.getConsoleOutputText());
+                buildDetailsVO.setDuration(buildWithDetails.getDuration());
+                buildDetailsVO.setResult(buildWithDetails.getResult());
+                buildDetailsVO.setTimestamps(buildWithDetails.getTimestamp());
+                buildDetailsVO.setArtifacts(buildWithDetails.getArtifacts());
+                return buildDetailsVO;
+            } catch (IOException e) {
+                throw ServiceException.BAD_REQUEST;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public String getEntity(String jobName, Integer buildId, String path) throws ServiceException {
+        BuildWithDetails buildWithDetails  = jenkinsApi.fetchBuildDetails(jobName, buildId, true);
+        return jenkinsApi.get(buildWithDetails, path);
+    }
+
+    @Override
+    public InputStream getArtifact(String jobName, Integer buildId, String path) throws ServiceException {
+        BuildWithDetails buildWithDetails  = jenkinsApi.fetchBuildDetails(jobName, buildId, true);
+        return jenkinsApi.downloadArtifact(buildWithDetails, path);
+    }
+
+    @Override
+    public void invokeBuild(Integer pipelineId, String jobName, Map<String, String> params) throws ServiceException {
+        Pipeline pipeline = pipelineDAO.findById(pipelineId).orElse(null);
+        if (pipeline == null) {
+            throw ServiceException.BAD_REQUEST;
+        }
+        if (StringUtils.isEmpty(jobName)) {
+            throw ServiceException.BAD_REQUEST;
+        }
+        PipelineVO vo = PipelineConverter.convertToVO(pipeline);
+        if (!CollectionUtils.isEmpty(vo.getParams())) {
+            vo.getParams().forEach(entry -> {
+                if (!params.containsKey(entry.getName())) {
+                    params.put(entry.getName(), entry.getDefaultValue() == null ? null : entry.getDefaultValue());
+                }
+            });
+        }
+        jenkinsApi.invokeBuild(jobName, params);
+    }
+}

+ 85 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/service/impl/PipelineServiceImpl.java

@@ -0,0 +1,85 @@
+package cn.seecoder.build.server.service.impl;
+
+import cn.seecoder.build.model.vo.ParamVO;
+import cn.seecoder.build.server.data.dao.PipelineDAO;
+import cn.seecoder.build.server.data.entity.Pipeline;
+import cn.seecoder.build.server.service.PipelineService;
+import cn.seecoder.build.server.service.model.converter.PipelineConverter;
+import cn.seecoder.build.model.vo.PipelineVO;
+import cn.seecoder.build.server.util.LoggerUtil;
+import cn.seecoder.build.server.util.ServiceException;
+import cn.seecoder.build.server.util.XmlUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.transaction.Transactional;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class PipelineServiceImpl implements PipelineService {
+
+    private static final Logger logger = LoggerUtil.getLogger(PipelineServiceImpl.class);
+
+    private final PipelineDAO pipelineDAO;
+
+
+    @Autowired
+    public PipelineServiceImpl(PipelineDAO pipelineDAO) {
+        this.pipelineDAO = pipelineDAO;
+    }
+
+    @Override
+    @Transactional
+    public PipelineVO createOrUpdate(PipelineVO vo) throws ServiceException {
+        List<ParamVO> paramVOS = new ArrayList<>();
+        try {
+            Pipeline pipeline = PipelineConverter.convertToEntity(vo);
+            Document document = XmlUtil.parseString(pipeline.getScript());
+            NodeList nList = document.getElementsByTagName("hudson.model.TextParameterDefinition");
+            if (nList != null) {
+                for (int i = 0; i < nList.getLength(); i++) {
+                    Node node = nList.item(i);
+                    paramVOS.add(new ParamVO((Element) node));
+                }
+            }
+            ObjectMapper objectMapper = new ObjectMapper();
+            pipeline.setParams(objectMapper.writeValueAsString(paramVOS));
+            PipelineVO result = PipelineConverter.convertToVO(pipelineDAO.save(pipeline));
+            return result;
+        } catch (JsonProcessingException e) {
+            logger.error("json write error, params={}, vo={}", e, paramVOS, vo);
+            throw new ServiceException("103", e.getMessage());
+        }
+    }
+
+    @Override
+    @Transactional
+    public void delete(Integer id) {
+        pipelineDAO.deleteById(id);
+    }
+
+    @Override
+    public List<PipelineVO> getAll() throws ServiceException {
+        return pipelineDAO.findAll().stream().map(PipelineConverter::convertToVO).collect(Collectors.toList());
+    }
+
+    @Override
+    public PipelineVO getDetailById(Integer id) throws ServiceException {
+        Pipeline pipeline = pipelineDAO.findById(id).orElse(null);
+        if (pipeline == null) {
+            return null;
+        }
+        PipelineVO vo = PipelineConverter.convertToVO(pipeline);
+        return vo;
+    }
+
+}

+ 39 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/service/model/converter/PipelineConverter.java

@@ -0,0 +1,39 @@
+package cn.seecoder.build.server.service.model.converter;
+
+import cn.seecoder.build.model.vo.ParamVO;
+import cn.seecoder.build.server.data.entity.Pipeline;
+import cn.seecoder.build.model.vo.PipelineVO;
+import cn.seecoder.build.server.util.LoggerUtil;
+import cn.seecoder.build.server.util.ServiceException;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.springframework.beans.BeanUtils;
+
+import java.util.List;
+
+public class PipelineConverter {
+
+    private static final Logger logger = LoggerUtil.getLogger(PipelineConverter.class);
+
+    public static Pipeline convertToEntity(PipelineVO pipelineVO) {
+        Pipeline pipeline = new Pipeline();
+        BeanUtils.copyProperties(pipelineVO, pipeline ,"params", "createdAt", "modifiedAt");
+        return pipeline;
+    }
+
+    public static PipelineVO convertToVO(Pipeline pipeline) throws ServiceException {
+        try {
+            PipelineVO pipelineVO = new PipelineVO();
+            BeanUtils.copyProperties(pipeline, pipelineVO, "params");
+            ObjectMapper mapper = new ObjectMapper();
+            JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, ParamVO.class);
+            List<ParamVO> params = mapper.readValue(pipeline.getParams(), type);
+            pipelineVO.setParams(params);
+            return pipelineVO;
+        } catch (Exception e) {
+            logger.error("convert error", e, pipeline);
+            throw ServiceException.INVALID_DATA;
+        }
+    }
+}

+ 30 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/util/ApplicationProperties.java

@@ -0,0 +1,30 @@
+package cn.seecoder.build.server.util;
+
+import lombok.Data;
+import org.hibernate.validator.constraints.URL;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+
+@Data
+@Configuration
+@ConfigurationProperties("seecoder-build-server")
+public class ApplicationProperties {
+
+    private String token;
+
+    @Valid
+    private Jenkins jenkins = new Jenkins();
+    @Data
+    public static class Jenkins {
+        @URL
+        @NotEmpty
+        private String host;
+        @NotEmpty
+        private String username = "root";
+        @NotEmpty
+        private String token;
+    }
+}

+ 26 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/util/AsyncConfig.java

@@ -0,0 +1,26 @@
+package cn.seecoder.build.server.util;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+@Configuration
+@EnableAsync
+public class AsyncConfig {
+
+    @Bean("asyncExecutor")
+    public Executor executor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(10);
+        executor.setMaxPoolSize(10);
+        executor.setQueueCapacity(Integer.MAX_VALUE);
+        executor.setThreadNamePrefix("async-");
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.initialize();
+        return executor;
+    }
+}

+ 34 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/util/AsyncWrapper.java

@@ -0,0 +1,34 @@
+package cn.seecoder.build.server.util;
+
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+@Component
+@CommonsLog
+public class AsyncWrapper {
+
+    @FunctionalInterface
+    public interface Method {
+        void invoke() throws Exception;
+    }
+
+    @Async("asyncExecutor")
+    public void asyncInvoke(Method method) {
+        try {
+            method.invoke();
+        } catch (Exception e) {
+            log.error("进行异步调用时发生异常[" + e.getClass() + "]:" + e.getMessage());
+        }
+    }
+
+    @Async("asyncExecutor")
+    public void asyncInvoke(Method method, long delay) {
+        try {
+            Thread.sleep(delay);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+        asyncInvoke(method);
+    }
+}

+ 91 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/util/JwtTokenUtil.java

@@ -0,0 +1,91 @@
+package cn.seecoder.build.server.util;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+@ConfigurationProperties(prefix = "seecoder-build-server.jwt")
+@Component
+public class JwtTokenUtil {
+    private String secret;
+    private Long expiration;
+
+    public String generateToken(UserDetails userDetails) {
+        Map<String, Object> claims = new HashMap<>(2);
+        claims.put("sub", userDetails.getUsername());
+        claims.put("created", new Date());
+        return generateToken(claims);
+    }
+
+    public String getUsernameFromToken(String token) {
+        String username;
+        try {
+            Claims claims = getClaimsFromToken(token);
+            username = claims.getSubject();
+        } catch (Exception e) {
+            username = null;
+        }
+        return username;
+    }
+
+    public Boolean isTokenExpired(String token) {
+        try {
+            Claims claims = getClaimsFromToken(token);
+            Date expiration = claims.getExpiration();
+            return expiration.before(new Date());
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 刷新令牌
+     *
+     * @param token 原令牌
+     * @return 新令牌
+     */
+    public String refreshToken(String token) {
+        String refreshedToken;
+        try {
+            Claims claims = getClaimsFromToken(token);
+            claims.put("created", new Date());
+            refreshedToken = generateToken(claims);
+        } catch (Exception e) {
+            refreshedToken = null;
+        }
+        return refreshedToken;
+    }
+
+    public Boolean validateToken(String token, UserDetails userDetails) {
+        String username = getUsernameFromToken(token);
+        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
+    }
+
+    private String generateToken(Map<String, Object> claims) {
+        Date expirationDate = new Date(System.currentTimeMillis() + expiration);
+        return Jwts.builder().setClaims(claims)
+                .setExpiration(expirationDate)
+                .signWith(SignatureAlgorithm.HS512, secret)
+                .compact();
+    }
+
+    private Claims getClaimsFromToken(String token) {
+        Claims claims;
+        try {
+            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
+        } catch (Exception e) {
+            claims = null;
+        }
+        return claims;
+    }
+
+}

+ 36 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/util/LoggerUtil.java

@@ -0,0 +1,36 @@
+package cn.seecoder.build.server.util;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LoggerUtil {
+
+    public static Logger getLogger(Class<?> className){
+        return LoggerFactory.getLogger(className);
+    }
+
+    public static void info(Logger logger, String info, Object... parameters){
+        logger.info(info, parameters);
+    }
+
+    public static void warn(Logger logger, String info, Object... parameters) {
+        logger.warn(info, parameters);
+    }
+
+    public static void warn(Logger logger, Exception e, String info, Object... parameters) {
+        logger.warn("", e);
+        logger.warn(info, parameters);
+    }
+
+    public static void  error(Logger logger, String info, Object... parameters) {
+        logger.error(info, parameters);
+    }
+
+    public static void error(Logger logger, Exception e, String info, Object... parameters) {
+        logger.error("", e);
+        logger.error(info, parameters);
+    }
+
+    public static void debug(Logger logger, String info, Object... parameters) {
+        logger.debug(info, parameters);
+    }
+}

+ 27 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/util/ServiceException.java

@@ -0,0 +1,27 @@
+package cn.seecoder.build.server.util;
+
+import lombok.Getter;
+
+@Getter
+public class ServiceException extends RuntimeException {
+
+    public static final ServiceException INVALID_DATA = new ServiceException("400", "不合法的数据");
+
+    public static final ServiceException BAD_REQUEST = new ServiceException("400", "不合法的请求");
+
+    private String code;
+
+    private String msg;
+
+    public ServiceException(String code, String msg) {
+        super(msg);
+        this.code = code;
+        this.msg = msg;
+    }
+
+    public ServiceException(String code, String msg, Throwable cause) {
+        super(msg, cause);
+        this.code = code;
+        this.msg = msg;
+    }
+}

+ 25 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/util/XmlUtil.java

@@ -0,0 +1,25 @@
+package cn.seecoder.build.server.util;
+
+import org.slf4j.Logger;
+import org.w3c.dom.Document;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.ByteArrayInputStream;
+
+public class XmlUtil {
+    private static final Logger logger = LoggerUtil.getLogger(XmlUtil.class);
+
+    public static Document parseString(String xml) throws ServiceException {
+        try {
+            ByteArrayInputStream input = new ByteArrayInputStream(xml.getBytes("UTF-8"));
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document document = builder.parse(input);
+            return document;
+        } catch (Exception e) {
+            logger.error("Pipeline createOrUpdate GetBytes Error!", e);
+            throw new ServiceException("102", "xml parse error!");
+        }
+    }
+}

+ 34 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/web/CustomerExceptionHandler.java

@@ -0,0 +1,34 @@
+package cn.seecoder.build.server.web;
+
+import cn.seecoder.build.server.util.ServiceException;
+import cn.seecoder.build.server.web.model.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+
+
+@ControllerAdvice
+public class CustomerExceptionHandler {
+
+    private static final Logger logger = LoggerFactory.getLogger(CustomerExceptionHandler.class);
+
+    @ExceptionHandler(value = ServiceException.class)
+    @ResponseBody
+    public Response serviceExceptionHandler(HttpServletRequest req, ServiceException e) {
+        e.printStackTrace();
+        logger.error("Error: ", e);
+        return Response.buildFailure(e);
+    }
+
+    @ExceptionHandler(value = Exception.class)
+    @ResponseBody
+    public Response exceptionHandler(HttpServletRequest req, Exception e){
+        e.printStackTrace();
+        logger.error("Error: ", e);
+        return Response.buildFailure(e);
+    }
+}

+ 49 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/web/WebSecurityConfig.java

@@ -0,0 +1,49 @@
+package cn.seecoder.build.server.web;
+
+import cn.seecoder.build.server.web.security.JwtAuthenticationTokenFilter;
+import cn.seecoder.build.server.web.security.User;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+    private final JwtAuthenticationTokenFilter filter;
+
+    @Autowired
+    public WebSecurityConfig(JwtAuthenticationTokenFilter filter) {
+        super();
+        this.filter = filter;
+    }
+
+    @Override
+    protected void configure(HttpSecurity http) throws Exception {
+        http
+                .cors()
+                .and()
+                .sessionManagement()
+                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                .and()
+                .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
+                .csrf().disable()
+                .authorizeRequests()
+                .antMatchers("/api/auth/**").permitAll()
+                .antMatchers("/api/**").hasAnyAuthority(User.ADMIN_AUTHORITY.getAuthority(), User.TOKEN_AUTHORITY.getAuthority())
+                .and();
+    }
+
+    @Bean
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+}

+ 57 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/web/controller/api/JobController.java

@@ -0,0 +1,57 @@
+package cn.seecoder.build.server.web.controller.api;
+
+import cn.seecoder.build.server.service.JobService;
+import cn.seecoder.build.server.util.ServiceException;
+import cn.seecoder.build.server.web.model.Response;
+import org.apache.commons.io.IOUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api")
+public class JobController {
+
+    private final JobService jobService;
+
+    @Autowired
+    public JobController(JobService jobService) {
+        this.jobService = jobService;
+    }
+
+    @PostMapping("/job/run")
+    public Response invokeBuild(@RequestBody Map<String, Object> body) throws ServiceException {
+        jobService.invokeBuild((Integer)body.get("pipelineId"), (String)body.get("jobName"), (Map<String,String>)body.get("params"));
+        return Response.buildSuccess(null);
+    }
+
+    @PostMapping("/job")
+    public Response create(@RequestBody Map<String, Object> body) throws ServiceException {
+        return Response.buildSuccess(jobService.createJob((Integer)body.get("pipelineId"), (String) body.get("jobName")));
+    }
+
+    @GetMapping("/job/entity")
+    public Response getEntity(@RequestParam("path") String path, @RequestParam("jobName") String jobName, @RequestParam("buildId") Integer buildId) {
+        return Response.buildSuccess(jobService.getEntity(jobName, buildId, path));
+    }
+
+    @GetMapping(value = "/job/artifact", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+    @ResponseBody
+    public byte[] getArtifact(@RequestParam("path") String path, @RequestParam("jobName") String jobName, @RequestParam("buildId") Integer buildId) throws IOException {
+        return IOUtils.toByteArray(jobService.getArtifact(jobName, buildId, path));
+    }
+
+    @GetMapping("/job/{jobId}/build/{buildId}")
+    public Response getDetail(@PathVariable("jobId") String jobId, @PathVariable("buildId") Integer buildId) throws ServiceException {
+        return Response.buildSuccess(jobService.getBuildDetails(jobId, buildId));
+    }
+
+    @DeleteMapping("/job/{id}")
+    public Response delete(@PathVariable("id") String id) {
+        jobService.deleteJob(id);
+        return Response.buildSuccess(null);
+    }
+}

+ 47 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/web/controller/api/PipelineController.java

@@ -0,0 +1,47 @@
+package cn.seecoder.build.server.web.controller.api;
+
+import cn.seecoder.build.server.service.PipelineService;
+import cn.seecoder.build.model.vo.PipelineVO;
+import cn.seecoder.build.server.util.ServiceException;
+import cn.seecoder.build.server.web.model.Response;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/pipeline")
+public class PipelineController {
+
+    private final PipelineService pipelineService;
+
+    @Autowired
+    public PipelineController(PipelineService pipelineService) {
+        this.pipelineService = pipelineService;
+    }
+
+    @PostMapping("")
+    public Response create(@RequestBody  PipelineVO vo) throws ServiceException {
+        return Response.buildSuccess(pipelineService.createOrUpdate(vo));
+    }
+
+    @PutMapping("/{id}")
+    public Response update(@PathVariable("id") Integer id, @RequestBody  PipelineVO vo) throws ServiceException {
+        vo.setId(id);
+        return Response.buildSuccess(pipelineService.createOrUpdate(vo));
+    }
+
+    @GetMapping("")
+    public Response getAll() {
+        return Response.buildSuccess(pipelineService.getAll());
+    }
+
+    @GetMapping("/{id}")
+    public Response get(@PathVariable("id") Integer id) throws ServiceException {
+        return Response.buildSuccess(pipelineService.getDetailById(id));
+    }
+
+    @DeleteMapping("/{id}")
+    public Response delete(@PathVariable("id") Integer id) {
+        pipelineService.delete(id);
+        return Response.buildSuccess(null);
+    }
+}

+ 38 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/web/model/Response.java

@@ -0,0 +1,38 @@
+package cn.seecoder.build.server.web.model;
+
+import cn.seecoder.build.server.util.ServiceException;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Response {
+
+    private String code;
+
+    private String msg;
+
+    private Object result;
+
+    public static Response buildSuccess(Object result) {
+        return new Response("0", "", result);
+    }
+
+    public static  Response buildFailure(ServiceException e) {
+        return new Response(e.getCode(), e.getMsg(), null);
+    }
+
+    public static  Response buildFailure(Exception e) {
+        return new Response("-1", e.getLocalizedMessage(), null);
+    }
+
+    public static Response buildFailure(String code, String message) {
+        return new Response(code, message, null);
+    }
+
+    public static Response buildFailure(String message) {
+        return new Response("-1", message, null);
+    }
+}

+ 29 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/AuthController.java

@@ -0,0 +1,29 @@
+package cn.seecoder.build.server.web.security;
+
+import cn.seecoder.build.server.web.model.Response;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/auth")
+public class AuthController {
+
+    private final JwtAuthService jwtAuthService;
+
+    @Autowired
+    public AuthController(JwtAuthService jwtAuthService) {
+        this.jwtAuthService = jwtAuthService;
+    }
+
+    @PostMapping("")
+    public Response login(@RequestBody Map<String, String> map) {
+        String username = map.get("username");
+        String password = map.get("password");
+        return Response.buildSuccess(jwtAuthService.login(username, password));
+    }
+}

+ 39 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/JwtAuthService.java

@@ -0,0 +1,39 @@
+package cn.seecoder.build.server.web.security;
+
+import cn.seecoder.build.server.util.JwtTokenUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class JwtAuthService {
+
+    private final AuthenticationManager authenticationManager;
+
+    private final UserDetailsService userDetailsService;
+
+    private final JwtTokenUtil jwtTokenUtil;
+
+    @Autowired
+    public JwtAuthService(AuthenticationManager authenticationManager, @Qualifier("seecoder-build-serverUserDetailService") UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
+        this.authenticationManager = authenticationManager;
+        this.userDetailsService = userDetailsService;
+        this.jwtTokenUtil = jwtTokenUtil;
+    }
+
+    public String login(String username, String password) {
+        UsernamePasswordAuthenticationToken upToken =
+                new UsernamePasswordAuthenticationToken(username, password);
+        Authentication authentication = authenticationManager.authenticate(upToken);
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+        return jwtTokenUtil.generateToken(userDetails);
+    }
+
+}

+ 70 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/JwtAuthenticationTokenFilter.java

@@ -0,0 +1,70 @@
+package cn.seecoder.build.server.web.security;
+
+import cn.seecoder.build.server.util.JwtTokenUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
+
+    private final UserDetailsService userDetailsService;
+
+    private final JwtTokenUtil jwtTokenUtil;
+
+    @Autowired
+    public JwtAuthenticationTokenFilter(JwtTokenUtil jwtTokenUtil, @Qualifier("seecoder-build-serverUserDetailService") UserDetailsService userDetailsService) {
+        this.jwtTokenUtil = jwtTokenUtil;
+        this.userDetailsService = userDetailsService;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
+        String authHeader = httpServletRequest.getHeader("Authorization");
+        if (!StringUtils.isEmpty(authHeader)) {
+            String username = jwtTokenUtil.getUsernameFromToken(authHeader);
+            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
+                if (jwtTokenUtil.validateToken(authHeader, userDetails)) {
+                    //加载用户、角色、权限信息,Spring Security根据这些信息判断接口的访问权限
+                    UsernamePasswordAuthenticationToken authentication
+                            = new UsernamePasswordAuthenticationToken(userDetails, null,
+                            userDetails.getAuthorities());
+                    authentication.setDetails(new WebAuthenticationDetailsSource()
+                            .buildDetails(httpServletRequest));
+                    SecurityContextHolder.getContext().setAuthentication(authentication);
+                }
+            } else if (username == null) {
+                try {
+                    UserDetails userDetails = this.userDetailsService.loadUserByUsername(authHeader);
+                    if (userDetails != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+                        UsernamePasswordAuthenticationToken authentication
+                                = new UsernamePasswordAuthenticationToken(userDetails, null,
+                                userDetails.getAuthorities());
+                        authentication.setDetails(new WebAuthenticationDetailsSource()
+                                .buildDetails(httpServletRequest));
+                        SecurityContextHolder.getContext().setAuthentication(authentication);
+                    }
+                } catch (UsernameNotFoundException e) {
+                    httpServletResponse.getWriter().print("{\"code\":403,\"msg\":\"Unthorization\",\"result\":null}");
+                    httpServletResponse.setStatus(403);
+                }
+            }
+        }
+        filterChain.doFilter(httpServletRequest, httpServletResponse);
+    }
+}

+ 18 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/Sha256HexPasswordEncoder.java

@@ -0,0 +1,18 @@
+package cn.seecoder.build.server.web.security;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+
+@Component
+public class Sha256HexPasswordEncoder implements PasswordEncoder {
+    @Override
+    public String encode(CharSequence rawPassword) {
+        return DigestUtils.sha256Hex(rawPassword != null ? rawPassword.toString() : StringUtils.EMPTY);
+    }
+
+    @Override
+    public boolean matches(CharSequence rawPassword, String encodedPassword) {
+        return StringUtils.equals(encode(rawPassword), encodedPassword);
+    }
+}

+ 75 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/User.java

@@ -0,0 +1,75 @@
+package cn.seecoder.build.server.web.security;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+public class User implements UserDetails {
+
+    private String username;
+
+    private String password;
+
+    private List<GrantedAuthority> authorities;
+
+
+    public static final GrantedAuthority ADMIN_AUTHORITY = new SimpleGrantedAuthority("seecoder-build-server_ADMIN");
+
+    public static final GrantedAuthority TOKEN_AUTHORITY = new SimpleGrantedAuthority("seecoder-build-server_TOKEN");
+
+    public User(String username, String password, List<GrantedAuthority> authorities) {
+        this.username = username;
+        this.password = DigestUtils.sha256Hex(password);
+        this.authorities = authorities;
+    }
+
+    public User(String token) {
+        this.username = "seecoder-token";
+        this.password = "token";
+        this.authorities = Collections.singletonList(TOKEN_AUTHORITY);
+    }
+
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return authorities;
+    }
+
+    @Override
+    public String getPassword() {
+        return password;
+    }
+
+    @Override
+    public String getUsername() {
+        return username;
+    }
+
+    @Override
+    public boolean isAccountNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isAccountNonLocked() {
+        return true;
+    }
+
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return true;
+    }
+}

+ 36 - 0
seecoder-build-server/src/main/java/cn/seecoder/build/server/web/security/UserDetailServiceImpl.java

@@ -0,0 +1,36 @@
+package cn.seecoder.build.server.web.security;
+
+import cn.seecoder.build.server.util.ApplicationProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.security.SecurityProperties;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+
+@Service("seecoder-build-serverUserDetailService")
+public class UserDetailServiceImpl implements UserDetailsService {
+
+    private final SecurityProperties securityProperties;
+
+    private final ApplicationProperties applicationProperties;
+
+    @Autowired
+    public UserDetailServiceImpl(SecurityProperties securityProperties, ApplicationProperties applicationProperties) {
+        this.securityProperties = securityProperties;
+        this.applicationProperties = applicationProperties;
+    }
+
+    @Override
+    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
+        if (s.equals(securityProperties.getUser().getName())) {
+            return new User(s, securityProperties.getUser().getPassword(), Collections.singletonList(User.ADMIN_AUTHORITY));
+        } else if (s.equals(applicationProperties.getToken())) {
+            return new User(s);
+        } else {
+            throw new UsernameNotFoundException("未知用户");
+        }
+    }
+}

+ 30 - 0
seecoder-build-server/src/main/resources/application.yml

@@ -0,0 +1,30 @@
+server:
+  port: 8080
+spring:
+  datasource:
+    url: jdbc:mysql://172.19.240.15:3306/seecoder_build?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
+    username: root
+    password: 1qaz2wsx
+  jpa:
+    hibernate:
+      ddl-auto: update
+      naming:
+        implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
+        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
+    database-platform: org.hibernate.dialect.MySQL57InnoDBDialect
+    #    show-sql: true
+  #      hibernate.format_sql: true
+  security:
+    user:
+      name: root
+      password: NJU67software
+
+seecoder-build-server:
+  token: 3b861b052fd085887b5462fe5ae0c3a916b2c281d776a4d87e7bd5572ad24608
+  jwt:
+    secret: ${random.value}
+    expiration: 3600000
+  jenkins:
+    host: http://172.19.240.15:8081
+    username: admin
+    token: 1109a1c52db7e455ef4d9c14226760905d

+ 20 - 0
seecoder-build-server/src/test/java/cn/seecoder/build/server/AppTest.java

@@ -0,0 +1,20 @@
+package cn.seecoder.build.server;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Unit test for simple App.
+ */
+public class AppTest 
+{
+    /**
+     * Rigorous Test :-)
+     */
+    @Test
+    public void shouldAnswerWithTrue()
+    {
+        assertTrue( true );
+    }
+}

+ 13 - 0
settings.xml

@@ -0,0 +1,13 @@
+<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
+					http://maven.apache.org/xsd/settings-1.0.0.xsd">
+    <mirrors>
+        <mirror>
+            <id>aliyunmaven</id>
+            <mirrorOf>*</mirrorOf>
+            <name>阿里云公共仓库</name>
+            <url>https://maven.aliyun.com/repository/public</url>
+        </mirror>
+    </mirrors>
+</settings>