Java tutorial
/* * The MIT License * * Copyright (c) 2016 CloudBees, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * */ package jenkins.scm.impl.mock; import hudson.FilePath; import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.UUID; import java.util.WeakHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import jenkins.scm.api.SCMFile; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; public class MockSCMController implements Closeable { private static Map<String, MockSCMController> instances = new WeakHashMap<String, MockSCMController>(); private String id = UUID.randomUUID().toString(); private Map<String, Repository> repositories = new TreeMap<String, Repository>(); private String displayName; private String description; private String url; private String repoIconClassName; private String orgIconClassName; private MockSCMController() { } public static MockSCMController create() { MockSCMController c = new MockSCMController(); synchronized (instances) { instances.put(c.id, c); } return c; } public static List<MockSCMController> all() { synchronized (instances) { return new ArrayList<MockSCMController>(instances.values()); } } public static MockSCMController lookup(String id) { synchronized (instances) { return instances.get(id); } } public String getId() { return id; } @Override public void close() { synchronized (instances) { instances.remove(id); } repositories.clear(); } public String getRepoIconClassName() { return repoIconClassName; } public void setRepoIconClassName(String repoIconClassName) { this.repoIconClassName = repoIconClassName; } public String getOrgIconClassName() { return orgIconClassName; } public void setOrgIconClassName(String orgIconClassName) { this.orgIconClassName = orgIconClassName; } public String getDescription() throws IOException { return description; } public void setDescription(String description) throws IOException { this.description = description; } public String getDisplayName() throws IOException { return displayName; } public void setDisplayName(String displayName) throws IOException { this.displayName = displayName; } public String getUrl() throws IOException { return url; } public void setUrl(String url) throws IOException { this.url = url; } public synchronized void createRepository(String name) throws IOException { repositories.put(name, new Repository()); createBranch(name, "master"); } public synchronized void deleteRepository(String name) throws IOException { repositories.remove(name); } public synchronized List<String> listRepositories() throws IOException { return new ArrayList<String>(repositories.keySet()); } public String getDescription(String repository) throws IOException { return resolve(repository).description; } public void setDescription(String repository, String description) throws IOException { resolve(repository).description = description; } public String getDisplayName(String repository) throws IOException { return resolve(repository).displayName; } public void setDisplayName(String repository, String displayName) throws IOException { resolve(repository).displayName = displayName; } public String getUrl(String repository) throws IOException { return resolve(repository).url; } public void setUrl(String repository, String url) throws IOException { resolve(repository).url = url; } public synchronized void createBranch(String repository, String branch) throws IOException { State state = new State(); Repository repo = resolve(repository); repo.revisions.put(state.getHash(), state); repo.heads.put(branch, state.getHash()); } public synchronized void cloneBranch(String repository, String srcBranch, String dstBranch) throws IOException { Repository repo = resolve(repository); repo.heads.put(dstBranch, repo.heads.get(srcBranch)); } public synchronized void deleteBranch(String repository, String branch) throws IOException { resolve(repository).heads.remove(branch); } public synchronized void createTag(String repository, String branch, String tag) throws IOException { Repository repo = resolve(repository); repo.tags.put(tag, resolve(repository, branch).getHash()); } public synchronized void deleteTag(String repository, String tag) throws IOException { resolve(repository).tags.remove(tag); } public synchronized Integer openChangeRequest(String repository, String branch) throws IOException { Repository repo = resolve(repository); String hash = resolve(repository, branch).getHash(); Integer crNum = ++repo.lastChangeRequest; repo.changes.put(crNum, hash); repo.changeBaselines.put(crNum, branch); return crNum; } public synchronized void closeChangeRequest(String repository, Integer crNum) throws IOException { resolve(repository).changes.remove(crNum); resolve(repository).changeBaselines.remove(crNum); } public synchronized String getTarget(String repository, Integer crNum) throws IOException { return resolve(repository).changeBaselines.get(crNum); } public synchronized List<String> listBranches(String repository) throws IOException { return new ArrayList<String>(resolve(repository).heads.keySet()); } public synchronized List<String> listTags(String repository) throws IOException { return new ArrayList<String>(resolve(repository).tags.keySet()); } public synchronized List<Integer> listChangeRequests(String repository) throws IOException { return new ArrayList<Integer>(resolve(repository).changes.keySet()); } public synchronized String getRevision(String repository, String branch) throws IOException { return resolve(repository, branch).getHash(); } public synchronized void addFile(String repository, String branchOrCR, String message, String path, byte[] content) throws IOException { Repository repo = resolve(repository); String branchName; Integer crNum; String hash; // check branch first hash = repo.heads.get(branchOrCR); if (hash == null) { branchName = null; Matcher m = Pattern.compile("change-request/(\\d+)").matcher(branchOrCR); if (m.matches()) { crNum = Integer.valueOf(m.group(1)); hash = repo.changes.get(crNum); if (hash == null) { throw new IOException("Unknown change request: " + crNum + " in repository " + repository); } } else { throw new IOException("Unknown branch: " + branchOrCR + " in repository " + repository); } } else { branchName = branchOrCR; crNum = null; } State base = repo.revisions.get(hash); State state = new State(base, message, Collections.singletonMap(path, content), Collections.<String>emptySet()); repo.revisions.put(state.getHash(), state); if (branchName != null) { repo.heads.put(branchName, state.getHash()); } if (crNum != null) { repo.changes.put(crNum, state.getHash()); } } public synchronized void rmFile(String repository, String branchOrCR, String message, String path) throws IOException { Repository repo = resolve(repository); String branchName; Integer crNum; String hash; // check branch first hash = repo.heads.get(branchOrCR); if (hash == null) { branchName = null; Matcher m = Pattern.compile("change-request/(\\d+)").matcher(branchOrCR); if (m.matches()) { crNum = Integer.valueOf(m.group(1)); hash = repo.changes.get(crNum); if (hash == null) { throw new IOException("Unknown change request: " + crNum + " in repository " + repository); } } else { throw new IOException("Unknown branch: " + branchOrCR + " in repository " + repository); } } else { branchName = branchOrCR; crNum = null; } State base = repo.revisions.get(hash); State state = new State(base, message, Collections.<String, byte[]>emptyMap(), Collections.<String>singleton(path)); repo.revisions.put(state.getHash(), state); if (branchName != null) { repo.heads.put(branchName, state.getHash()); } if (crNum != null) { repo.changes.put(crNum, state.getHash()); } } public synchronized String checkout(File workspace, String repository, String identifier) throws IOException { State state = resolve(repository, identifier); for (Map.Entry<String, byte[]> entry : state.files.entrySet()) { FileUtils.writeByteArrayToFile(new File(workspace, entry.getKey()), entry.getValue()); } return state.getHash(); } public synchronized String checkout(FilePath workspace, String repository, String identifier) throws IOException, InterruptedException { State state = resolve(repository, identifier); for (Map.Entry<String, byte[]> entry : state.files.entrySet()) { workspace.child(entry.getKey()).copyFrom(new ByteArrayInputStream(entry.getValue())); } return state.getHash(); } private synchronized State resolve(String repository, String identifier) throws IOException { Repository repo = resolve(repository); // check hash first String hash = repo.revisions.containsKey(identifier) ? identifier : null; if (hash != null) { return repo.revisions.get(hash); } // now check for a named branch hash = repo.heads.get(identifier); if (hash != null) { return repo.revisions.get(hash); } // now check for a named tag hash = repo.tags.get(identifier); if (hash != null) { return repo.revisions.get(hash); } // now check for a change request Matcher m = Pattern.compile("change-request/(\\d+)").matcher(identifier); if (m.matches()) { Integer crNum = Integer.valueOf(m.group(1)); hash = repo.changes.get(crNum); if (hash != null) { return repo.revisions.get(hash); } throw new IOException("Unknown change request: " + crNum + " in repository " + repository); } throw new IOException("Unknown branch/tag/revision: " + identifier + " in repository " + repository); } private Repository resolve(String repository) throws IOException { Repository repo = repositories.get(repository); if (repo == null) { throw new IOException("Unknown repository: " + repository); } return repo; } public synchronized List<LogEntry> log(String repository, String identifier) throws IOException { State state = resolve(repository, identifier); List<LogEntry> result = new ArrayList<LogEntry>(); while (state != null) { result.add(new LogEntry(state.getHash(), state.timestamp, state.message)); state = state.parent; } return result; } public synchronized SCMFile.Type stat(String repository, String identifier, String path) throws IOException { State state = resolve(repository, identifier); if (state == null) { return SCMFile.Type.NONEXISTENT; } if (state.files.containsKey(path)) { return SCMFile.Type.REGULAR_FILE; } for (String p : state.files.keySet()) { if (p.startsWith(path + "/")) { return SCMFile.Type.DIRECTORY; } } return SCMFile.Type.NONEXISTENT; } public synchronized long lastModified(String repository, String identifier) { try { State state = resolve(repository, identifier); if (state == null) { return 0L; } return state.timestamp; } catch (IOException e) { return 0L; } } private static class Repository { private Map<String, State> revisions = new TreeMap<String, State>(); private Map<String, String> heads = new TreeMap<String, String>(); private Map<String, String> tags = new TreeMap<String, String>(); private Map<Integer, String> changes = new TreeMap<Integer, String>(); private Map<Integer, String> changeBaselines = new TreeMap<Integer, String>(); private int lastChangeRequest; private String description; private String displayName; private String url; } private static class State { private final State parent; private final String message; private final long timestamp; private final Map<String, byte[]> files; private transient String hash; public State() { this.parent = null; this.message = null; this.timestamp = System.currentTimeMillis(); this.files = new TreeMap<String, byte[]>(); } public State(State parent, String message, Map<String, byte[]> added, Set<String> removed) { this.parent = parent; this.message = message; this.timestamp = System.currentTimeMillis(); Map<String, byte[]> files = parent != null ? new TreeMap<String, byte[]>(parent.files) : new TreeMap<String, byte[]>(); files.keySet().removeAll(removed); files.putAll(added); this.files = files; } public String getHash() { if (hash == null) { try { Charset utf8 = Charset.forName("UTF-8"); MessageDigest sha = MessageDigest.getInstance("SHA-1"); if (parent != null) { sha.update(new BigInteger(parent.getHash(), 16).toByteArray()); } sha.update(StringUtils.defaultString(message).getBytes(utf8)); sha.update((byte) (timestamp & 0xff)); sha.update((byte) ((timestamp >> 8) & 0xff)); sha.update((byte) ((timestamp >> 16) & 0xff)); sha.update((byte) ((timestamp >> 24) & 0xff)); sha.update((byte) ((timestamp >> 32) & 0xff)); sha.update((byte) ((timestamp >> 40) & 0xff)); sha.update((byte) ((timestamp >> 48) & 0xff)); sha.update((byte) ((timestamp >> 56) & 0xff)); for (Map.Entry<String, byte[]> e : files.entrySet()) { sha.update(e.getKey().getBytes(utf8)); sha.update(e.getValue()); } hash = javax.xml.bind.DatatypeConverter.printHexBinary(sha.digest()).toLowerCase(); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("SHA-1 message digest mandated by JLS"); } } return hash; } } public static final class LogEntry { private final String hash; private final long timestamp; private final String message; private LogEntry(String hash, long timestamp, String message) { this.hash = hash; this.timestamp = timestamp; this.message = message; } public String getHash() { return hash; } public long getTimestamp() { return timestamp; } public String getMessage() { return message; } @Override public String toString() { return String.format("Commit %s%nDate: %tc%n%s%n", hash, timestamp, message); } } }