GuardOfDawn hace 7 años
commit
4de1297ea6

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.idea
+target
+*.iml
+config-dev.yml

+ 1 - 0
README.md

@@ -0,0 +1 @@
+# SEEC-II后端demo

+ 33 - 0
config.yml

@@ -0,0 +1,33 @@
+server:
+  port: 8080
+spring:
+  datasource:
+    url: jdbc:mysql://localhost:3306/seec2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
+    username: root
+    password: 123456
+  jpa:
+    database-platform: org.hibernate.dialect.MySQL57InnoDBDialect
+  mail:
+    host: smtp.yandex.com
+    username: noreply@moocoder.cn
+    password: 1143757638qaz
+
+moocoder:
+  secret: 1qaz2wsx
+  gitlab:
+    host: http://10.1.1.172:8081
+    git-proxy: http://10.1.1.172:8081
+    username: root
+    token: 1hXhC2YLkuAb9iaykqWi
+  jenkins:
+    host: http://10.1.1.243:8082
+    username: root
+    token: 113cb5bd4a6fea342892a08c8d2e07dc68
+  docker:
+    registry: 10.1.1.173:5000
+  mail:
+    from: noreply@moocoder.cn
+  storage:
+    type: local
+    properties:
+      location: ./problem

+ 145 - 0
pom.xml

@@ -0,0 +1,145 @@
+<?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.1.1.RELEASE</version>
+		<relativePath/> <!-- lookup parent from repository -->
+	</parent>
+	<groupId>nju.seec</groupId>
+	<artifactId>SEEC-demo</artifactId>
+	<version>0.0.1-SNAPSHOT</version>
+	<name>SEEC-demo</name>
+	<description>Demo project for Spring Boot</description>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<java.version>1.8</java.version>
+	</properties>
+
+	<dependencies>
+		<!-- Spring Boot Auto-Configurable Dependencies -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-actuator</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-web</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-configuration-processor</artifactId>
+		</dependency>
+
+		<!-- Hibernate Dependencies -->
+		<dependency>
+			<groupId>org.hibernate</groupId>
+			<artifactId>hibernate-java8</artifactId>
+		</dependency>
+
+		<!-- Apache Commons Dependencies -->
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-lang3</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>commons-codec</groupId>
+			<artifactId>commons-codec</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-compress</artifactId>
+			<version>1.16.1</version>
+		</dependency>
+
+		<!-- Api Dependencies -->
+		<dependency>
+			<groupId>com.google.guava</groupId>
+			<artifactId>guava</artifactId>
+			<version>20.0</version>
+		</dependency>
+		<dependency>
+			<groupId>org.gitlab4j</groupId>
+			<artifactId>gitlab4j-api</artifactId>
+			<version>4.8.7</version>
+		</dependency>
+		<dependency>
+			<groupId>com.offbytwo.jenkins</groupId>
+			<artifactId>jenkins-client</artifactId>
+			<version>0.3.7</version>
+		</dependency>
+		<dependency>
+			<groupId>com.spotify</groupId>
+			<artifactId>docker-client</artifactId>
+			<version>8.11.2</version>
+		</dependency>
+
+		<!-- Utility Dependencies -->
+		<dependency>
+			<groupId>org.json</groupId>
+			<artifactId>json</artifactId>
+			<version>20180130</version>
+		</dependency>
+		<dependency>
+			<groupId>org.eclipse.jgit</groupId>
+			<artifactId>org.eclipse.jgit</artifactId>
+			<version>4.11.0.201803080745-r</version>
+		</dependency>
+		<dependency>
+			<groupId>org.zeroturnaround</groupId>
+			<artifactId>zt-zip</artifactId>
+			<version>1.12</version>
+		</dependency>
+
+		<!-- Database Driver Dependencies -->
+		<dependency>
+			<groupId>mysql</groupId>
+			<artifactId>mysql-connector-java</artifactId>
+			<scope>runtime</scope>
+		</dependency>
+
+		<!-- Lombok Dependencies -->
+		<dependency>
+			<groupId>org.projectlombok</groupId>
+			<artifactId>lombok</artifactId>
+			<optional>true</optional>
+		</dependency>
+
+		<!-- Test Dependencies -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>

+ 23 - 0
src/main/java/nju/seec/SEECdemo/SeecDemoApplication.java

@@ -0,0 +1,23 @@
+package nju.seec.SEECdemo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+import java.time.ZoneId;
+import java.util.TimeZone;
+
+@SpringBootApplication
+@EnableJpaRepositories(basePackages = "nju.seec.SEECdemo.data.dao")
+@EnableJpaAuditing
+public class SeecDemoApplication {
+
+	public static void main(String[] args) {
+		// 强制写入默认时区
+		TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("GMT+8")));
+		SpringApplication.run(SeecDemoApplication.class, args);
+	}
+
+}
+

+ 5 - 0
src/main/java/nju/seec/SEECdemo/logic/api/DockerApi.java

@@ -0,0 +1,5 @@
+package nju.seec.SEECdemo.logic.api;
+
+public interface DockerApi {
+	void build(String directory, String imageName, String imageTag) throws Exception;
+}

+ 48 - 0
src/main/java/nju/seec/SEECdemo/logic/api/GitlabApi.java

@@ -0,0 +1,48 @@
+package nju.seec.SEECdemo.logic.api;
+
+import nju.seec.SEECdemo.logic.api.vo.GitlabUser;
+import org.gitlab4j.api.GitLabApiException;
+import org.gitlab4j.api.models.CommitAction;
+
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Set;
+
+public interface GitlabApi {
+	GitlabUser createUser(String username, String email, String password) throws GitLabApiException;
+
+	Integer createProject(String name) throws GitLabApiException;
+
+	Integer forkProject(int userId, int projectId, int namespaceId) throws GitLabApiException;
+
+	Set<String> compare(int projectId, String from, String to) throws GitLabApiException;
+
+	void archiveProject(int projectId) throws GitLabApiException;
+
+	void changePassword(int userId, String password) throws GitLabApiException;
+
+	void deleteUser(int userId) throws GitLabApiException;
+
+	void deleteProject(int projectId) throws GitLabApiException;
+
+	/**
+	 * get file content
+	 * @param projectId
+	 * @param branch
+	 * @param path
+	 * @return
+	 */
+	String getRepositoryFile(int projectId, String branch, String path) throws GitLabApiException, UnsupportedEncodingException;
+
+	/**
+	 * create a commit
+	 * @param projectId
+	 * @param branch
+	 * @param commitMessage
+	 * @param email
+	 * @param username
+	 * @param actions
+	 */
+	void createCommit(int projectId, String branch, String commitMessage, String email, String username, List<CommitAction> actions) throws GitLabApiException;
+
+}

+ 18 - 0
src/main/java/nju/seec/SEECdemo/logic/api/JenkinsApi.java

@@ -0,0 +1,18 @@
+package nju.seec.SEECdemo.logic.api;
+
+import com.offbytwo.jenkins.model.QueueItem;
+import nju.seec.SEECdemo.logic.api.vo.BuildDetails;
+
+import java.io.IOException;
+import java.util.Map;
+
+public interface JenkinsApi {
+	void createJob(int id) throws IOException;
+
+	QueueItem invokeBuild(int id, Map<String, String> paramMap) throws IOException;
+
+	void deleteJob(int id) throws IOException;
+
+	// waitFinished: 是否等待直到构建完全完成
+	BuildDetails fetchBuildDetails(int id, int buildNumber, String target, boolean waitFinished) throws IOException;
+}

+ 59 - 0
src/main/java/nju/seec/SEECdemo/logic/api/impl/DockerApiImpl.java

@@ -0,0 +1,59 @@
+package nju.seec.SEECdemo.logic.api.impl;
+
+import com.spotify.docker.client.DefaultDockerClient;
+import com.spotify.docker.client.DockerClient;
+import com.spotify.docker.client.DockerClient.BuildParam;
+import com.spotify.docker.client.ProgressHandler;
+import com.spotify.docker.client.auth.ConfigFileRegistryAuthSupplier;
+import com.spotify.docker.client.auth.RegistryAuthSupplier;
+import com.spotify.docker.client.messages.ProgressMessage;
+import lombok.extern.apachecommons.CommonsLog;
+import nju.seec.SEECdemo.logic.api.DockerApi;
+import nju.seec.SEECdemo.util.ApplicationProperties;
+import nju.seec.SEECdemo.util.ApplicationProperties.Docker;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.io.IOException;
+
+@Component
+@CommonsLog
+public class DockerApiImpl implements DockerApi {
+	private final String registry;
+	private final DockerClient client;
+	private final ProgressHandler handler;
+	private final RegistryAuthSupplier registryAuthSupplier;
+
+	@Autowired
+	public DockerApiImpl(ApplicationProperties properties) {
+		Docker docker = properties.getDocker();
+		registry = docker.getRegistry();
+		client = new DefaultDockerClient(docker.getHost());
+		handler = new NoActionProgressHandler();
+		registryAuthSupplier = new ConfigFileRegistryAuthSupplier();
+	}
+
+	public void build(String directory, String imageName, String imageTag) throws Exception {
+		String imageId = client.build(new File(directory).toPath(), handler, BuildParam.name(registry + "/" + imageName + ":" + imageTag), BuildParam.forceRm());
+		if (imageId == null) {
+			throw new IOException("Failed to build docker image under [" + directory + "]");
+		}
+		client.push(registry + "/" + imageName + ":" + imageTag, new LogActionProgressHandler(), registryAuthSupplier.authFor(registry + "/" + imageName + ":" + imageTag));
+	}
+
+	private class NoActionProgressHandler implements ProgressHandler {
+		@Override
+		public void progress(ProgressMessage message) {
+			//IGNORE THE MESSAGE
+		}
+	}
+
+	private class LogActionProgressHandler implements ProgressHandler {
+		@Override
+		public void progress(ProgressMessage message) {
+			if(message.error()!=null)
+				log.error(message.error());
+		}
+	}
+}

+ 150 - 0
src/main/java/nju/seec/SEECdemo/logic/api/impl/GitlabApiImpl.java

@@ -0,0 +1,150 @@
+package nju.seec.SEECdemo.logic.api.impl;
+
+import nju.seec.SEECdemo.logic.api.GitlabApi;
+import nju.seec.SEECdemo.logic.api.vo.GitlabUser;
+import nju.seec.SEECdemo.util.ApplicationProperties;
+import nju.seec.SEECdemo.util.ApplicationProperties.Gitlab;
+import nju.seec.SEECdemo.util.ToolKit;
+import org.gitlab4j.api.CommitsApi;
+import org.gitlab4j.api.GitLabApi;
+import org.gitlab4j.api.GitLabApiException;
+import org.gitlab4j.api.RepositoryFileApi;
+import org.gitlab4j.api.models.*;
+import org.gitlab4j.api.models.ImpersonationToken.Scope;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Base64;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Component
+public class GitlabApiImpl implements GitlabApi {
+	private final Gitlab gitlab;
+
+	private final GitLabApi server;
+
+	@Autowired
+	public GitlabApiImpl(ApplicationProperties properties) {
+		gitlab = properties.getGitlab();
+		this.server = new GitLabApi(gitlab.getHost(), gitlab.getToken());
+	}
+
+	@Override
+	public synchronized GitlabUser createUser(String username, String email, String password) throws GitLabApiException {
+		User user = new User();
+		user.setUsername(username);
+		user.setEmail(email);
+		user.setName(username);
+		user.setSkipConfirmation(true);
+		user = server.getUserApi().createUser(user, password, 1000);
+		GitlabUser gitlabUser = new GitlabUser();
+		gitlabUser.setId(user.getId());
+		gitlabUser.setNamespace(fetchNamespace(username));
+		gitlabUser.setToken(createToken(user.getId()));
+		return gitlabUser;
+	}
+
+	private Integer fetchNamespace(String username) throws GitLabApiException {
+		Namespace namespace;
+		namespace = server.getNamespaceApi().findNamespaces(username)
+				.stream()
+				.filter(ns -> "user".equals(ns.getKind()))
+				.reduce((ns1, ns2) -> ns1.getId() > ns2.getId() ? ns1 : ns2)
+				.orElse(null);
+		return namespace != null ? namespace.getId() : null;
+	}
+
+	private String createToken(int userId) throws GitLabApiException {
+		Scope[] scopes = new Scope[]{Scope.API};
+		ImpersonationToken token;
+		token = server.getUserApi().createImpersonationToken(userId, "AES-" + ToolKit.randomUUID(), null, scopes);
+		return token.getToken();
+	}
+
+	@Override
+	public synchronized Integer createProject(String name) throws GitLabApiException {
+		Project project = new Project();
+		project.setName(name);
+		project.setVisibility(Visibility.INTERNAL);
+		project = server.getProjectApi().createProject(project);
+		return project != null ? project.getId() : null;
+	}
+
+	@Override
+	public synchronized Integer forkProject(int userId, int projectId, int namespaceId) throws GitLabApiException {
+		Project project;
+		try {
+			server.setSudoAsId(userId);
+			project = server.getProjectApi().forkProject(projectId, namespaceId);
+			project.setVisibility(Visibility.PRIVATE);
+			project = server.getProjectApi().updateProject(project);
+		} finally {
+			server.unsudo();
+		}
+		if (project != null) {
+			createWebHook(project.getId());
+		}
+		return project != null ? project.getId() : null;
+	}
+
+	@Override
+	public synchronized Set<String> compare(int projectId, String from, String to) throws GitLabApiException {
+		CompareResults compareResults = server.getRepositoryApi().compare(projectId, from, to);
+		return compareResults.getDiffs().stream()
+				.map(Diff::getNewPath)
+				.map(p -> {
+					int end = p.indexOf('/');
+					if (end == -1) {
+						end = p.length();
+					}
+					return p.substring(0, end);
+				})
+				.collect(Collectors.toSet());
+	}
+
+	private void createWebHook(int id) throws GitLabApiException {
+		String url = gitlab.getWebHookProxy() + "/internal/notify/webhook/" + id;
+		server.getProjectApi().addHook(id, url, true, false, false);
+	}
+
+	@Override
+	public synchronized void archiveProject(int projectId) throws GitLabApiException {
+		server.getProjectApi().archiveProject(projectId);
+	}
+
+	@Override
+	public synchronized void changePassword(int userId, String password) throws GitLabApiException {
+		User user = server.getUserApi().getUser(userId);
+		server.getUserApi().modifyUser(user, password, user.getProjectsLimit());
+	}
+
+	@Override
+	public synchronized void deleteUser(int userId) throws GitLabApiException {
+		server.getUserApi().deleteUser(userId, true);
+	}
+
+	@Override
+	public void deleteProject(int projectId) throws GitLabApiException {
+		server.getProjectApi().deleteProject(projectId);
+	}
+
+	@Override
+	public String getRepositoryFile(int projectId, String branch, String path) throws GitLabApiException, UnsupportedEncodingException {
+		RepositoryFileApi repositoryFileApi = server.getRepositoryFileApi();
+		RepositoryFile repositoryFile = repositoryFileApi.getFile(path, projectId, branch);
+		if(repositoryFile!=null){
+			return new String(Base64.getDecoder().decode(repositoryFile.getContent()), "UTF-8");
+		}
+		return null;
+	}
+
+	@Override
+	public void createCommit(int projectId, String branch, String commitMessage, String email, String username, List<CommitAction> actions) throws GitLabApiException {
+		CommitsApi commitsApi = server.getCommitsApi();
+        commitsApi.createCommit(projectId, branch, commitMessage, null, email, username,actions);
+    }
+
+}

+ 160 - 0
src/main/java/nju/seec/SEECdemo/logic/api/impl/JenkinsApiImpl.java

@@ -0,0 +1,160 @@
+package nju.seec.SEECdemo.logic.api.impl;
+
+import com.offbytwo.jenkins.JenkinsServer;
+import com.offbytwo.jenkins.client.JenkinsHttpClient;
+import com.offbytwo.jenkins.model.Artifact;
+import com.offbytwo.jenkins.model.BuildWithDetails;
+import com.offbytwo.jenkins.model.QueueItem;
+import com.offbytwo.jenkins.model.QueueReference;
+import lombok.extern.apachecommons.CommonsLog;
+import nju.seec.SEECdemo.logic.api.JenkinsApi;
+import nju.seec.SEECdemo.logic.api.vo.BuildDetails;
+import nju.seec.SEECdemo.logic.api.vo.CoverageResult;
+import nju.seec.SEECdemo.logic.api.vo.MutationResult;
+import nju.seec.SEECdemo.util.ApplicationProperties;
+import nju.seec.SEECdemo.util.ApplicationProperties.Jenkins;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.dom4j.*;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Map;
+
+@Component
+@CommonsLog
+public class JenkinsApiImpl implements JenkinsApi {
+	private final ApplicationProperties properties;
+
+	private JenkinsServer server;
+	private String configTemplate;
+
+	public JenkinsApiImpl(ApplicationProperties properties) {
+		this.properties = properties;
+	}
+
+	@PostConstruct
+	private void initialize() throws URISyntaxException, IOException {
+		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);
+		configTemplate = readTemplate();
+	}
+
+	@Override
+	public void createJob(int id) throws IOException {
+		server.createJob(String.valueOf(id), configTemplate);
+	}
+
+	@Override
+	public QueueItem invokeBuild(int id, Map<String, String> paramMap) throws IOException {
+		QueueReference reference;
+		if (paramMap == null) {
+			reference = server.getJob(String.valueOf(id)).build();
+		} else {
+			// 0.3.7版本的Jenkins客户端库中build(Map<String, String> params)方法存在BUG,会触发两次构建
+			reference = server.getJob(String.valueOf(id)).build(paramMap, 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;
+	}
+
+	@Override
+	public void deleteJob(int id) throws IOException {
+		server.deleteJob(String.valueOf(id));
+	}
+
+	@Override
+	public BuildDetails fetchBuildDetails(int id, int buildNumber, String target, boolean waitFinished) throws IOException {
+		BuildDetails result = new BuildDetails();
+		BuildWithDetails build;
+		build = server.getJob(String.valueOf(id)).getBuildByNumber(buildNumber).details();
+		while (waitFinished && (build.getResult() == null || build.getDuration() == 0)) {
+			try {
+				Thread.sleep(500);
+			} catch (InterruptedException e) {
+				Thread.currentThread().interrupt();
+			}
+			build = server.getJob(String.valueOf(id)).getBuildByNumber(buildNumber).details();
+		}
+		result.setConsoleOutput(build.getConsoleOutputText());
+		result.setNumber(build.getNumber());
+		result.setDuration(build.getDuration());
+		result.setBuildResult(build.getResult());
+		fetchTargetResult(result, build, target);
+		return result;
+	}
+
+	private void fetchTargetResult(BuildDetails result, BuildWithDetails build, String target) {
+		try {
+			if ("TEST".equals(target)) {
+				fetchTestResult(result, build);
+			} else if ("COVERAGE".equals(target)) {
+				fetchCoverageResult(result, build);
+			} else if ("MUTATION".equals(target)) {
+				fetchMutationResult(result, build);
+			}
+		} catch (Exception ignore) { }
+	}
+
+	private void fetchTestResult(BuildDetails result, BuildWithDetails build) throws IOException {
+		result.setTestResult(build.getTestResult());
+	}
+
+	private void fetchCoverageResult(BuildDetails result, BuildWithDetails build) throws IOException {
+		result.setCoverageResult(build.getClient().get(build.getUrl() + "/cobertura/?depth=2", CoverageResult.class));
+	}
+
+	private void fetchMutationResult(BuildDetails result, BuildWithDetails build) throws IOException, URISyntaxException, DocumentException {
+		List<Artifact> artifactList = build.getArtifacts();
+		Artifact artifact = artifactList.stream().filter(a -> a.getFileName().equals("mutations.xml")).findFirst().orElse(null);
+		if (artifact != null) {
+			MutationResult mutationResult = new MutationResult();
+			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+			outputStream.write(build.downloadArtifact(artifact));
+			String mutationReport = new String(outputStream.toByteArray());
+			Document document = DocumentHelper.parseText(mutationReport);
+			Element root = document.getRootElement();
+			for (Object element : root.elements("mutation")) {
+				if (element instanceof Element) {
+					Attribute attribute = ((Element) element).attribute("detected");
+					if (attribute != null) {
+						mutationResult.setMutations(mutationResult.getMutations() + 1);
+						if ("true".equals(attribute.getValue())) {
+							mutationResult.setDetectedMutations(mutationResult.getDetectedMutations() + 1);
+						}
+					}
+				}
+			}
+			result.setMutationResult(mutationResult);
+		}
+	}
+
+	private String readTemplate() throws IOException {
+		StringBuilder stringBuilder = new StringBuilder();
+		InputStream inputStream = JenkinsApiImpl.class.getClassLoader().getResourceAsStream("jenkins/config.xml");
+		BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+		String buffer;
+		while ((buffer = bufferedReader.readLine()) != null) {
+			stringBuilder.append(buffer).append("\n");
+		}
+		return stringBuilder.toString();
+	}
+}

+ 38 - 0
src/main/java/nju/seec/SEECdemo/logic/api/impl/PoolingJenkinsHttpClient.java

@@ -0,0 +1,38 @@
+package nju.seec.SEECdemo.logic.api.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();
+	}
+}

+ 18 - 0
src/main/java/nju/seec/SEECdemo/logic/api/vo/BuildDetails.java

@@ -0,0 +1,18 @@
+package nju.seec.SEECdemo.logic.api.vo;
+
+import com.offbytwo.jenkins.model.BuildResult;
+import com.offbytwo.jenkins.model.TestResult;
+import lombok.Data;
+import lombok.ToString;
+
+@Data
+@ToString(exclude = "consoleOutput")
+public class BuildDetails {
+	private int number;
+	private long duration;
+	private BuildResult buildResult;
+	private String consoleOutput;
+	private TestResult testResult;
+	private CoverageResult coverageResult;
+	private MutationResult mutationResult;
+}

+ 27 - 0
src/main/java/nju/seec/SEECdemo/logic/api/vo/CoverageResult.java

@@ -0,0 +1,27 @@
+package nju.seec.SEECdemo.logic.api.vo;
+
+import com.offbytwo.jenkins.model.BaseModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.List;
+
+@Setter
+@EqualsAndHashCode(callSuper = true)
+@ToString
+public class CoverageResult extends BaseModel {
+	private Results results;
+
+	public List<CoverageResultElement> getElements() {
+		return results.getElements();
+	}
+
+	@Data
+	@EqualsAndHashCode
+	@ToString
+	private static class Results {
+		private List<CoverageResultElement> elements;
+	}
+}

+ 11 - 0
src/main/java/nju/seec/SEECdemo/logic/api/vo/CoverageResultElement.java

@@ -0,0 +1,11 @@
+package nju.seec.SEECdemo.logic.api.vo;
+
+import lombok.Data;
+
+@Data
+public class CoverageResultElement {
+	private String name;
+	private int numerator;
+	private int denominator;
+	private int ratio;
+}

+ 10 - 0
src/main/java/nju/seec/SEECdemo/logic/api/vo/GitlabUser.java

@@ -0,0 +1,10 @@
+package nju.seec.SEECdemo.logic.api.vo;
+
+import lombok.Data;
+
+@Data
+public class GitlabUser {
+	private Integer id;
+	private Integer namespace;
+	private String token;
+}

+ 9 - 0
src/main/java/nju/seec/SEECdemo/logic/api/vo/MutationResult.java

@@ -0,0 +1,9 @@
+package nju.seec.SEECdemo.logic.api.vo;
+
+import lombok.Data;
+
+@Data
+public class MutationResult {
+	private int mutations;
+	private int detectedMutations;
+}

+ 88 - 0
src/main/java/nju/seec/SEECdemo/util/ApplicationProperties.java

@@ -0,0 +1,88 @@
+package nju.seec.SEECdemo.util;
+
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+import org.hibernate.validator.constraints.URL;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.Valid;
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+@Configuration
+@ConfigurationProperties("moocoder")
+@Validated
+public class ApplicationProperties {
+	@Pattern(regexp = "^[a-zA-Z0-9]+$")
+	@NotEmpty
+	private String secret;
+	@Valid
+	private Gitlab gitlab = new Gitlab();
+	@Valid
+	private Jenkins jenkins = new Jenkins();
+	@Valid
+	private Docker docker = new Docker();
+	@Valid
+	private Mail mail = new Mail();
+	@Valid
+	private Storage storage = new Storage();
+
+	@Data
+	public static class Gitlab {
+		@URL
+		@NotEmpty
+		private String host;
+		private String gitProxy;
+		@URL
+		@NotEmpty
+		private String webHookProxy = "http://localhost:3000";
+		@NotEmpty
+		private String username = "root";
+		@NotEmpty
+		private String token;
+
+		public String getGitProxy() {
+			return StringUtils.defaultString(gitProxy, host);
+		}
+	}
+
+	@Data
+	public static class Jenkins {
+		@URL
+		@NotEmpty
+		private String host;
+		@NotEmpty
+		private String username = "root";
+		@NotEmpty
+		private String token;
+	}
+
+	@Data
+	public static class Docker {
+		@NotEmpty
+		private String host = "unix:///var/run/docker.sock";
+		@NotEmpty
+		private String registry;
+	}
+
+	@Data
+	public static class Mail {
+		@Email
+		@NotEmpty
+		private String from;
+		@NotEmpty
+		private String personal = "MOOCODER";
+	}
+
+	@Data
+	public static class Storage {
+		private String type = "local";
+		private Map<String, String> properties = new HashMap<>();
+	}
+}

+ 35 - 0
src/main/java/nju/seec/SEECdemo/util/ToolKit.java

@@ -0,0 +1,35 @@
+package nju.seec.SEECdemo.util;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.springframework.http.HttpStatus;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.UUID;
+
+public abstract class ToolKit {
+	public static final String VERSION = "0.4.5";
+
+	public static String format(Exception exception) {
+		return "[" + exception.getClass().getName() + "]:" + exception.getMessage();
+	}
+
+	public static HttpStatus httpStatus(HttpServletRequest request) {
+		Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
+		if (statusCode == null) {
+			return HttpStatus.INTERNAL_SERVER_ERROR;
+		}
+		try {
+			return HttpStatus.valueOf(statusCode);
+		} catch (Exception e) {
+			return HttpStatus.INTERNAL_SERVER_ERROR;
+		}
+	}
+
+	public static String randomUUID() {
+		return UUID.randomUUID().toString().replace("-", "");
+	}
+
+	public static String randomPassword() {
+		return RandomStringUtils.randomAlphanumeric(12);
+	}
+}

+ 45 - 0
src/main/resources/application.yml

@@ -0,0 +1,45 @@
+spring:
+  jpa:
+    hibernate:
+      ddl-auto: update
+      naming:
+        implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
+        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
+  mail:
+    properties:
+      mail:
+        smtp:
+          auth: true
+          starttls:
+            enable: true
+            required: true
+  resources:
+    chain:
+      strategy:
+        content:
+          enabled: true
+          paths:
+            - /moocoder-frontend/**
+            - /js/**
+            - /css/**
+  servlet:
+    multipart:
+      max-file-size: 16MB
+      max-request-size: 16MB
+server:
+  tomcat:
+    remote-ip-header: X-Forwarded-For
+    protocol-header: X-Forwarded-Proto
+  servlet:
+    session:
+      persistent: true
+      store-dir: ${java.io.tmpdir}/moocoder-session
+      timeout: 1d
+  use-forward-headers: true
+management:
+  endpoints:
+    web:
+      base-path: /api/actuator
+  endpoint:
+    shutdown:
+      enabled: true

+ 116 - 0
src/main/resources/jenkins/config.xml

@@ -0,0 +1,116 @@
+<?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>
+            </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;--network none --cpus 1 -m 1G -h localhost --mount type=tmpfs,destination=/var/ws/tmp,tmpfs-size=134217728&apos;
+                    reuseNode true
+                }
+            }
+            steps {
+                echo &apos;==CONSOLE OUTPUT BEGIN==&apos;
+                catchError {
+                    timeout(1) {
+                        sh &quot;${EXECUTE_SHELL}&quot;
+                    }
+                }
+                echo &apos;==CONSOLE OUTPUT END==&apos;
+            }
+        }
+        stage(&apos;Collect Test 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>

+ 17 - 0
src/test/java/nju/seec/SEECdemo/SeecDemoApplicationTests.java

@@ -0,0 +1,17 @@
+package nju.seec.SEECdemo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SeecDemoApplicationTests {
+
+	@Test
+	public void contextLoads() {
+	}
+
+}
+