diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aaeeb2d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/target/
+/work/
+/.classpath
+/.project
+/.settings
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index e366887..45bf28c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
org.jenkins-ci.plugins
unique-id
- 1.3-SNAPSHOT
+ 2.0-SNAPSHOT
hpi
Unique ID Library Plugin
http://wiki.jenkins-ci.org/display/JENKINS/Unique+Id+Plugin
@@ -17,9 +17,13 @@
scm:git:ssh://github.com/jenkinsci/unique-id-plugin.git
scm:git:ssh://git@github.com/jenkinsci/unique-id-plugin.git
https://github.com/jenkinsci/unique-id-plugin
- unique-id-1.2
+
+ 1.7
+ 1.7
+
+
org.jenkins-ci.plugins
@@ -27,6 +31,24 @@
4.0
true
+
+ org.jenkins-ci
+ test-annotations
+ 1.2
+ test
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+ 1.3
+ test
+
org.jenkins-ci.plugins
credentials
@@ -54,7 +76,15 @@
maven-release-plugin
- 2.4
+ 2.5.1
+
+
+ org.jenkins-ci.tools
+ maven-hpi-plugin
+ true
+
+ 2.0
+
diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/IdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/IdStore.java
index ea19209..55c85f5 100644
--- a/src/main/java/org/jenkinsci/plugins/uniqueid/IdStore.java
+++ b/src/main/java/org/jenkinsci/plugins/uniqueid/IdStore.java
@@ -1,16 +1,28 @@
package org.jenkinsci.plugins.uniqueid;
import hudson.ExtensionPoint;
+
import jenkins.model.Jenkins;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+
import javax.annotation.Nullable;
+import org.apache.commons.codec.binary.Base64;
+
/**
* An abstraction to persistently store and retrieve unique id's
* for various Jenkins model objects.
*
* These keys are guaranteed to be unique with a Jenkins
* and immutable across the lifetime of the given object.
+ *
+ * Implementations should not store the ID inside any specific item configuration as it is
+ * common for users top copy items either through the UI or manually and this will cause the
+ * IDs to become non-unique.
+ *
*
* @param
*/
@@ -26,14 +38,14 @@ public IdStore (Class forType) {
* Creates an unique id for the given object.
* Subsequent calls are idempotent.
*
- * @param object
+ * @param object the object to make the id for.
*/
public abstract void make(T object);
/**
* Get the id for this given object.
* @param object
- * @return the id or null if none assigned.
+ * @return the id or {@code null} if none assigned.
*/
@Nullable
public abstract String get(T object);
@@ -63,7 +75,7 @@ public static IdStore forClass(Class clazz) {
*
* @throws java.lang.IllegalArgumentException if the type is not supported.
*/
- public static void makeId(Object object) {
+ public static void makeId(Object object) throws IllegalArgumentException {
IdStore store = forClass(object.getClass());
if (store == null) {
throw new IllegalArgumentException("Unsupported type: " + object.getClass().getName());
@@ -77,7 +89,7 @@ public static void makeId(Object object) {
*
* @throws java.lang.IllegalArgumentException if the type is not supported.
*/
- public static String getId(Object object) {
+ public static String getId(Object object) throws IllegalArgumentException {
IdStore store = forClass(object.getClass());
if (store == null) {
throw new IllegalArgumentException("Unsupported type: " + object.getClass().getName());
@@ -86,4 +98,13 @@ public static String getId(Object object) {
}
}
+ /**
+ * Generates a new unique ID.
+ * Subclasses do not need to use this to create unique IDs and are free to create IDs by other methods.
+ * @return a string that should be unique against all jenkins instances.
+ */
+ protected static String generateUniqueID() {
+ return Base64.encodeBase64String(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)).substring(0, 30);
+ }
+
}
diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/FolderIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/FolderIdStore.java
index 3bb1115..814d166 100644
--- a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/FolderIdStore.java
+++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/FolderIdStore.java
@@ -1,37 +1,47 @@
package org.jenkinsci.plugins.uniqueid.impl;
-import com.cloudbees.hudson.plugins.folder.Folder;
-import com.cloudbees.hudson.plugins.folder.FolderProperty;
-import com.cloudbees.hudson.plugins.folder.FolderPropertyDescriptor;
import hudson.Extension;
import hudson.model.Action;
import hudson.model.Actionable;
-import org.jenkinsci.plugins.uniqueid.IdStore;
+import hudson.util.DescribableList;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
-import java.util.logging.Level;
+import java.util.Iterator;
import java.util.logging.Logger;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+import com.cloudbees.hudson.plugins.folder.FolderProperty;
+import com.cloudbees.hudson.plugins.folder.FolderPropertyDescriptor;
+import com.cloudbees.hudson.plugins.folder.Folder;
+
/**
* Stores ids for folders as a {@link FolderIdProperty}
+ * @deprecated {@see PersistenceRootIdStore}
*/
@Extension(optional = true)
-public class FolderIdStore extends IdStore {
+@Deprecated
+@Restricted(NoExternalUse.class)
+public class FolderIdStore extends LegacyIdStore {
public FolderIdStore() {
super(Folder.class);
}
@Override
- public void make(Folder folder) {
- if (folder.getProperties().get(FolderIdProperty.class) == null) {
- try {
- folder.addProperty(new FolderIdProperty());
- } catch (IOException e) {
- LOGGER.log(Level.SEVERE, "Failed to add property",e);
+ public void remove(Folder folder) throws IOException {
+ DescribableList,FolderPropertyDescriptor> properties = folder.getProperties();
+
+ for (Iterator> itr = properties.iterator(); itr.hasNext(); ) {
+ FolderProperty> prop = itr.next();
+
+ if (prop instanceof FolderIdProperty) {
+ itr.remove();
}
}
+ folder.save();
}
@Override
diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/Id.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/Id.java
index 6abc0f7..03cedef 100644
--- a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/Id.java
+++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/Id.java
@@ -1,18 +1,29 @@
package org.jenkinsci.plugins.uniqueid.impl;
+import jenkins.model.RunAction2;
+
import hudson.model.Action;
import hudson.model.Actionable;
+import hudson.model.Run;
+
import org.apache.commons.codec.binary.Base64;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
import javax.annotation.Nullable;
+
import java.util.UUID;
import java.util.logging.Logger;
/**
- * An action which stores an id.
+ * DO NOT USE
+ * @deprecated users should not use this as it ties the ID explicitly to the item and as will not work when copying items (for example).
*/
-class Id implements Action {
+@Deprecated
+@Restricted(NoExternalUse.class)
+class Id implements Action , RunAction2 {
+ private final static Logger LOGGER = Logger.getLogger(Id.class.getName());
private final String id;
@@ -36,7 +47,13 @@ public String getId() {
return id;
}
+
+ /**
+ * @deprecated Sub classes should not use this as it stores the ID in the actionable item.
+ * @return
+ */
@Nullable
+ @Deprecated
protected static String getId(Actionable actionable) {
Id id = actionable.getAction(Id.class);
if (id != null) {
@@ -47,5 +64,15 @@ protected static String getId(Actionable actionable) {
}
- private final static Logger LOGGER = Logger.getLogger(Id.class.getName());
+
+ public void onAttached(Run, ?> r) {
+ // NO-OP
+ }
+
+ /**
+ * Migrates the run away from using this Action.
+ */
+ public void onLoad(Run, ?> r) {
+ IdStoreMigratorV1ToV2.migrate(r);
+ }
}
diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2.java
new file mode 100644
index 0000000..1e0e7c8
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2.java
@@ -0,0 +1,166 @@
+package org.jenkinsci.plugins.uniqueid.impl;
+
+import hudson.Extension;
+import hudson.init.InitMilestone;
+import hudson.init.Initializer;
+import hudson.model.Item;
+import hudson.model.PersistenceRoot;
+import hudson.model.Job;
+import hudson.model.Run;
+
+import jenkins.model.Jenkins;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.annotation.Nonnull;
+
+import org.jenkinsci.plugins.uniqueid.implv2.PersistenceRootIdStore;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+/**
+ * Converts legacy UniqueIDs that are stored inside a Folder/Job/Run configuration to UniqueIDs that are stored alongside the Folder/Job/Run.
+ *
+ */
+@Restricted(NoExternalUse.class)
+@Extension
+public class IdStoreMigratorV1ToV2 {
+
+ private static Logger LOGGER = Logger.getLogger(IdStoreMigratorV1ToV2.class.getName());
+
+ private static final String MARKER_FILE_NAME = "unique-id-migration.txt";
+
+ /**
+ * Migrates any IDs stored in Folder/Job/Run configuration
+ * @throws IOException
+ */
+
+ @Initializer(after=InitMilestone.JOB_LOADED, before=InitMilestone.COMPLETED, fatal=true)
+ public static void migrateIdStore() throws IOException {
+ Jenkins jenkins = Jenkins.getInstance();
+ if (jenkins == null) {
+ throw new IllegalStateException("Jenkins is null, so it is impossible to migrate the IDs");
+ }
+ File marker = new File(jenkins.getRootDir(), MARKER_FILE_NAME);
+ if (marker.exists()) {
+ LOGGER.log(Level.INFO, "Migration of IDStore already performed, so skipping migration.");
+ return;
+ }
+ LOGGER.log(Level.INFO, "Starting migration of IDs");
+
+ performMigration(jenkins);
+
+ }
+
+ @SuppressWarnings("unchecked")
+ static void performMigration(@Nonnull Jenkins jenkins) {
+ List- allItems = jenkins.getAllItems();
+
+ for (Item item : allItems) {
+ // can only be Folder or Job here (not a run) - and these both implement PersistenceRoot
+ if (item instanceof PersistenceRoot) {
+ migrate((PersistenceRoot) item);
+ }
+ else {
+ LOGGER.log(Level.WARNING, "Expected item of type Folder or Job which implement PersistenceRoot, but got a {0} so can not migrate the IdStore for this item",
+ item.getClass().getName());
+ }
+ }
+ LOGGER.log(Level.INFO, "migration of unique IDs for Jobs and Folders complete - will continue to process Runs in the background.");
+
+ Thread t = new Thread(new RunIDMigrationThread(), "unique-id background migration thread");
+ t.setDaemon(true);
+ t.start();
+ }
+
+ static void migrate(PersistenceRoot pr) {
+ LOGGER.log(Level.FINE, "migrating {0}" , pr.toString());
+ try {
+ String id = LegacyIdStore.getId(pr);
+ if (id != null) {
+ PersistenceRootIdStore.create(pr, id);
+ LegacyIdStore.removeId(pr);
+ }
+ } catch (IOException ex) {
+ // need to rethrow (but add some context first) otherwise the migration will continue to run
+ // and it will not have migrated everything :-(
+ throw new IDStoreMigrationException("Failure whilst migrating " + pr.toString(), ex);
+ }
+ }
+
+ /**
+ * Exception to indicate a failure to migrate the IDStore.
+ */
+ private static class IDStoreMigrationException extends RuntimeException {
+
+ public IDStoreMigrationException(String message, Throwable cause) {
+ super(message,cause);
+ }
+ }
+
+ private static class RunIDMigrationThread implements Runnable {
+
+ public void run() {
+ Jenkins jenkins = Jenkins.getInstance();
+ if (jenkins == null) {
+ throw new IllegalStateException("Jenkins is null, so it is impossible to migrate the IDs");
+ }
+ // if new jobs are added that is ok - as their runs will not need to be migrated.
+ // if jobs are deleted from Jenkins we need to handle that fact!
+ List allJobs = jenkins.getAllItems(Job.class);
+ int totalJobs = allJobs.size();
+ int migratedJobs = 0;
+ int migratedBuilds = 0;
+ final long startTime = System.currentTimeMillis();
+ long lastLog = System.currentTimeMillis();
+ for (Job job : allJobs) {
+ // Force the loading of the builds.
+ migratedJobs++;
+ if (job.getConfigFile().getFile().exists()) {
+ // we have not been deleted!
+ for (Iterator iterator = job.getBuilds().iterator(); iterator.hasNext();) {
+ // the build is migrated by the action in Id.onLoad(Run)
+ // touch something in the build just to force loading incase it gets more lazy in the future.
+ Object r = iterator.next();
+ if (r != null && r instanceof Run) {
+ ((Run)r).getResult();
+ }
+ migratedBuilds++;
+ }
+ }
+ if ((System.currentTimeMillis() - lastLog) > (60 * 1000L) ) {
+ lastLog = System.currentTimeMillis();
+ LOGGER.log(Level.INFO, "Processed {0} builds, and have inspected all runs from {1} out of {2} jobs.",
+ new Object[] {migratedBuilds, migratedJobs, totalJobs});
+ }
+ }
+ // all done...
+ final long duration = System.currentTimeMillis() - startTime;
+ final long minutes = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - startTime);
+ final long seconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - startTime - TimeUnit.MINUTES.toMillis(minutes));
+
+ LOGGER.log(Level.INFO, "Finished unique-id migration of builds in {0} minutes {1} seconds. Processed {2} runs from {3} jobs.",
+ new Object[] {minutes, seconds, migratedBuilds, migratedJobs});
+ File marker = new File(jenkins.getRootDir(), MARKER_FILE_NAME);
+ try {
+ if (!marker.createNewFile()) {
+ LOGGER.log(Level.WARNING, "Failed to record the completion of the IDStore Migration. " +
+ "This will cause performance issues on subsequent startup. " +
+ "Please create an empty file at '" + marker.getCanonicalPath() + "'");
+ }
+ }
+ catch (IOException ex) {
+ LOGGER.log(Level.WARNING, "Failed to record the completion of the IDStore Migration. " +
+ "This will cause performance issues on subsequent startup. " +
+ "Please create an empty file in the Jenkins home directory called '" + MARKER_FILE_NAME + "'.", ex);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/JobIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/JobIdStore.java
index 5e5b2af..e7299cb 100644
--- a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/JobIdStore.java
+++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/JobIdStore.java
@@ -6,7 +6,6 @@
import hudson.model.Job;
import hudson.model.JobProperty;
import hudson.model.JobPropertyDescriptor;
-import org.jenkinsci.plugins.uniqueid.IdStore;
import java.io.IOException;
import java.util.Collection;
@@ -14,26 +13,31 @@
import java.util.logging.Level;
import java.util.logging.Logger;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
/**
* Stores ids for jobs in {@link JobIdProperty}
+ * @deprecated {@see PersistenceRootIdStore}
*/
@Extension
-public class JobIdStore extends IdStore {
+@Deprecated
+@Restricted(NoExternalUse.class)
+public class JobIdStore extends LegacyIdStore {
public JobIdStore() {
super(Job.class);
}
@Override
- public void make(Job job) {
- if (job.getProperty(JobIdProperty.class) == null) {
- try {
- job.addProperty(new JobIdProperty());
- } catch (IOException e) {
- LOGGER.log(Level.SEVERE, "Failed to add property",e);
- }
+ public void remove(Job job) {
+ try {
+ while (job.removeProperty(JobIdProperty.class) != null) {}
+ } catch (IOException ex) {
+ LOGGER.log(Level.WARNING, "Failed to remove property from " + job.getFullName(), ex);
}
}
+
@Override
public String get(Job thing) {
return Id.getId((Actionable) thing);
@@ -43,6 +47,7 @@ public String get(Job thing) {
/**
* A unique Id for Jobs.
*/
+ @Deprecated
public static class JobIdProperty extends JobProperty> {
private Id id = new Id();
diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/LegacyIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/LegacyIdStore.java
new file mode 100644
index 0000000..11c38b6
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/LegacyIdStore.java
@@ -0,0 +1,101 @@
+package org.jenkinsci.plugins.uniqueid.impl;
+
+import hudson.ExtensionPoint;
+
+import jenkins.model.Jenkins;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+/**
+ * An abstraction to persistently store and retrieve unique id's
+ * for various Jenkins model objects.
+ *
+ * These keys are guaranteed to be unique with a Jenkins
+ * and immutable across the lifetime of the given object.
+ *
+ * Implementations should not store the ID inside any specific item configuration as it is
+ * common for users top copy items either through the UI or manually and this will cause the
+ * IDs to become non-unique.
+ *
+ *
+ * @param
+ */
+@Restricted(NoExternalUse.class)
+@Deprecated
+public abstract class LegacyIdStore implements ExtensionPoint {
+
+ private final Class type;
+
+ public LegacyIdStore (Class forType) {
+ this.type = forType;
+ }
+
+ /**
+ * Remove the unique id associated with the given object.
+ * @param object
+ */
+ public abstract void remove(T object) throws IOException;
+
+ /**
+ * Get the id for this given object.
+ * @param object
+ * @return the id or null if none assigned.
+ */
+ @Nullable
+ public abstract String get(T object);
+
+ public boolean supports(Class clazz) {
+ return type.isAssignableFrom(clazz);
+ }
+
+ /**
+ * Retrieve an {@link LegacyIdStore} for the given type
+ * @param clazz
+ * @param
+ * @return the store which supports the type, or null if none
+ */
+ @Nullable
+ public static LegacyIdStore forClass(Class clazz) {
+ for (LegacyIdStore store : Jenkins.getInstance().getExtensionList(LegacyIdStore.class)) {
+ if (store.supports(clazz)) {
+ return store;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Convenience method which makes the id for the given object.
+ *
+ * @throws java.lang.IllegalArgumentException if the type is not supported.
+ * @throws IOException if we could not remove the ID from the Object.
+ */
+ public static void removeId(Object object) throws IOException{
+ LegacyIdStore store = forClass(object.getClass());
+ if (store == null) {
+ throw new IllegalArgumentException("Unsupported type: " + object.getClass().getName());
+ } else {
+ store.remove(object);
+ }
+ }
+
+ /**
+ * Convenience method which retrieves the id for the given object.
+ *
+ * @throws java.lang.IllegalArgumentException if the type is not supported.
+ */
+ public static String getId(Object object) {
+ LegacyIdStore store = forClass(object.getClass());
+ if (store == null) {
+ throw new IllegalArgumentException("Unsupported type: " + object.getClass().getName());
+ } else {
+ return store.get(object);
+ }
+ }
+
+}
diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/RunIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/RunIdStore.java
index 1f8937c..b1f4eb0 100644
--- a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/RunIdStore.java
+++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/RunIdStore.java
@@ -1,39 +1,41 @@
package org.jenkinsci.plugins.uniqueid.impl;
import hudson.Extension;
+import hudson.model.Action;
import hudson.model.Actionable;
import hudson.model.Run;
-import org.jenkinsci.plugins.uniqueid.IdStore;
import java.io.IOException;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import java.util.Iterator;
+import java.util.List;
+
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
- * Stores id's for runs as an action on the Run.
+ * Controls id's for runs.
*/
@Extension
-public class RunIdStore extends IdStore {
+@Deprecated
+@Restricted(NoExternalUse.class)
+public class RunIdStore extends LegacyIdStore {
+
public RunIdStore() {
super(Run.class);
}
@Override
- public void make(Run run) {
- if (run.getAction(Id.class) == null) {
- run.addAction(new Id());
- try {
- run.save();
- } catch (IOException e) {
- LOGGER.log(Level.SEVERE,"Failed to save id",e);
- }
+ public void remove(Run run) throws IOException {
+ List actionList = run.getActions();
+ List ids = run.getActions(Id.class);
+ if (!ids.isEmpty()) {
+ actionList.removeAll(ids);
+ run.save();
}
}
+
@Override
public String get(Run thing) {
return Id.getId((Actionable) thing);
}
-
- private final static Logger LOGGER = Logger.getLogger(RunIdStore.class.getName());
-
}
diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/PersistenceRootIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/PersistenceRootIdStore.java
new file mode 100644
index 0000000..43068ee
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/PersistenceRootIdStore.java
@@ -0,0 +1,86 @@
+package org.jenkinsci.plugins.uniqueid.implv2;
+
+import hudson.Extension;
+import hudson.model.PersistenceRoot;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.commons.io.FileUtils;
+import org.jenkinsci.plugins.uniqueid.IdStore;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+
+/**
+ * The {@link PersistenceRootIdStore} allows the storing of a Unique ID for any PersistenceRoot item. This replaces the
+ * need for {@link FolderIdStore}, {@link JobIdStore} and {@link RunIdStore}
+ */
+@Extension
+public class PersistenceRootIdStore extends IdStore {
+
+ /** Our Logger. */
+ private final static Logger LOGGER = Logger.getLogger(PersistenceRootIdStore.class.getName());
+
+ /** The name of the file in which we store the unique ID. */
+ private final static String ID_FILE = "unique-id.txt";
+
+ public PersistenceRootIdStore() {
+ super(PersistenceRoot.class);
+ }
+
+ @Override
+ public void make(PersistenceRoot object) {
+ File f = new File(object.getRootDir(), ID_FILE);
+ if (!f.exists()) {
+ File tmp = null;
+ try {
+ tmp = File.createTempFile(".unique-id_", ".tmp", object.getRootDir());
+ FileUtils.writeStringToFile(f, IdStore.generateUniqueID(), StandardCharsets.UTF_8);
+ try {
+ Files.move(tmp.toPath(), f.toPath(), StandardCopyOption.ATOMIC_MOVE);
+ }
+ catch (FileAlreadyExistsException ignored) {
+ FileUtils.deleteQuietly(tmp);
+ return; // we already have an id.
+ }
+ }
+ catch (IOException ex) {
+ LOGGER.log(Level.WARNING, "Failed to store unique ID for " + object.toString(), ex);
+ }
+ }
+ }
+
+ @Override
+ public String get(PersistenceRoot object) {
+ File f = new File(object.getRootDir(), ID_FILE);
+ if (f.exists() && f.canRead()) {
+ try {
+ return FileUtils.readFileToString(f, StandardCharsets.UTF_8);
+ } catch (IOException ex) {
+ LOGGER.log(Level.WARNING, "Failed to retrieve unique ID for " + object.toString(), ex);
+ }
+ }
+ return null;
+ }
+
+ @Restricted(NoExternalUse.class)
+ public static void create(PersistenceRoot object, String uniqueId) throws IOException {
+ File f = new File(object.getRootDir(), ID_FILE);
+ if (!f.exists()) {
+ LOGGER.log(Level.FINE, "Creating file ({1}) to store ID for ({0}) whose RootDir is ({2}).", new Object[] {object.toString(), f, object.getRootDir()});
+ // no need to migrate if its there to begin with!
+ FileUtils.writeStringToFile(f, uniqueId, StandardCharsets.UTF_8);
+ }
+ else {
+ LOGGER.log(Level.FINE, "**NOT** creating file ({1}) to store ID for ({0}) whose RootDir is ({2}).", new Object[] {object.toString(), f, object.getRootDir()});
+ }
+ }
+
+}
diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/RunIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/RunIdStore.java
new file mode 100644
index 0000000..6d91cd8
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/RunIdStore.java
@@ -0,0 +1,48 @@
+package org.jenkinsci.plugins.uniqueid.implv2;
+
+import hudson.Extension;
+import hudson.model.PersistenceRoot;
+import hudson.model.Job;
+import hudson.model.Run;
+
+import org.jenkinsci.plugins.uniqueid.IdStore;
+
+/**
+ * Manages Unique IDs for Runs.
+ * Whilst we could use the {@link PersistenceRootIdStore} that will create extra files for
+ * every single build. A build already has a unique identifier (build number / build id) and a parent job can have a
+ * unique ID, so we build one from the parent and our build number to save creating a file.
+ */
+@Extension(ordinal=1) // needs to take priority over the PersistenceRootIdStore
+public class RunIdStore extends IdStore {
+ public RunIdStore() {
+ super(Run.class);
+ }
+
+ @Override
+ public void make(Run run) {
+ // we calculate these on the fly, or serve up migrated IDs if they exist.
+ // in order to calculate on the fly we require the parent to have an id.
+ IdStore.makeId(run.getParent());
+ }
+
+ @Override
+ public String get(Run run) {
+ IdStore persistenceStore = IdStore.forClass(PersistenceRoot.class);
+
+ String id = persistenceStore.get(run);
+ if (id != null) {
+ // migrated legacy id
+ return id;
+ }
+
+ // calculate the ID.
+ Job parent = run.getParent();
+ String parentID = IdStore.getId(parent);
+ if (parentID != null) {
+ return parentID + '_' + run.getId();
+ }
+ return null;
+ }
+
+}
diff --git a/src/test/java/org/jenkinsci/plugins/uniqueid/IdTest.java b/src/test/java/org/jenkinsci/plugins/uniqueid/IdTest.java
index a7fa1fe..3885c65 100644
--- a/src/test/java/org/jenkinsci/plugins/uniqueid/IdTest.java
+++ b/src/test/java/org/jenkinsci/plugins/uniqueid/IdTest.java
@@ -3,7 +3,6 @@
import com.cloudbees.hudson.plugins.folder.Folder;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
-import hudson.model.Job;
import hudson.model.Project;
import org.junit.Rule;
import org.junit.Test;
@@ -24,9 +23,14 @@ public void project() throws Exception {
String id = IdStore.getId(p);
AbstractBuild build = jenkinsRule.buildAndAssertSuccess(p);
- assertNull(IdStore.getId(build));
- IdStore.makeId(build);
+ // a build will get an id computed from its parent.
String buildId = IdStore.getId(build);
+ assertEquals(buildId, id+'_'+build.getId());
+
+ // should be a no-op
+ IdStore.makeId(build);
+ assertEquals(IdStore.getId(build), buildId);
+
jenkinsRule.jenkins.reload();
AbstractProject resurrectedProject = jenkinsRule.jenkins.getItemByFullName(p.getFullName(), AbstractProject.class);
diff --git a/src/test/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test.java b/src/test/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test.java
new file mode 100644
index 0000000..34f11b1
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test.java
@@ -0,0 +1,89 @@
+package org.jenkinsci.plugins.uniqueid.impl;
+
+import hudson.model.ItemGroup;
+import hudson.model.PersistenceRoot;
+import hudson.model.Job;
+import hudson.model.Run;
+
+import jenkins.model.Jenkins;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.io.FileUtils;
+import org.jenkinsci.plugins.uniqueid.IdStore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.recipes.LocalData;
+
+import com.cloudbees.hudson.plugins.folder.Folder;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertThat;
+
+public class IdStoreMigratorV1ToV2Test {
+
+ @Rule
+ public JenkinsRule jenkinsRule = new JenkinsRule();
+
+ @Test
+ @Issue("JENKINS-28843")
+ @LocalData
+ public void testMigration() throws Exception {
+ Jenkins jenkins = jenkinsRule.jenkins;
+ assertThat("All jobs loaded correctly", jenkins.getAllItems(), hasSize(4));
+
+ Folder folderNoID = jenkins.getItem("folderNoID", jenkins, Folder.class);
+ Folder folderWithID = jenkins.getItem("folderWithID", jenkins, Folder.class);
+
+ Job jobNoID = jenkins.getItem("jobNoID", (ItemGroup)folderWithID, Job.class);
+ Job jobWithID = jenkins.getItem("jobWithID", (ItemGroup)folderWithID, Job.class);
+
+ assertThat(folderNoID, notNullValue());
+ assertThat(folderWithID, notNullValue());
+ assertThat(jobNoID, notNullValue());
+ assertThat(jobWithID, notNullValue());
+
+ checkID(folderNoID, null);
+ checkID(folderWithID, "YzUxN2JiZTYtNGVhZS00NDQxLTg5NT");
+
+ checkID(jobNoID, null);
+ checkID(jobWithID, "ZGQxMDNhYzUtMTJlOC00YTc4LTgzOT");
+
+
+ checkID(jobNoID.getBuildByNumber(1), null);
+ checkID(jobNoID.getBuildByNumber(2), null);
+
+ // build 1 had no id so its generated on the fly from the parent
+ checkID(jobWithID.getBuildByNumber(1), "ZGQxMDNhYzUtMTJlOC00YTc4LTgzOT_" + jobWithID.getBuildByNumber(1).getId());
+ checkID(jobWithID.getBuildByNumber(2), "NGQ0ODM2NjktZGM0OS00MjdkLWE3NT");
+ }
+
+ private static void checkID(PersistenceRoot obj, String expectedID) throws Exception {
+ assertThat("Checking " + obj.toString(), IdStore.getId(obj), is(expectedID));
+ if (expectedID != null) {
+ File f = new File(obj.getRootDir(), "config.xml");
+
+ String string = FileUtils.readFileToString(f, StandardCharsets.UTF_8);
+ // main config should not contain a reference to the unique ID any more.
+ assertThat("config.xml for " + obj.toString() + " still contains the ID", string, not(containsString(expectedID)));
+ }
+ }
+
+ private static void checkID(Run obj, String expectedID) throws Exception {
+ assertThat("Checking " + obj.toString(), IdStore.getId(obj), is(expectedID));
+ if (expectedID != null) {
+ File f = new File(obj.getRootDir(), "build.xml");
+
+ String string = FileUtils.readFileToString(f, StandardCharsets.UTF_8);
+ // main config should not contain a reference to the unique ID any more.
+ assertThat("build.xml for " + obj.toString() + " still contains the ID", string, not(containsString(expectedID)));
+ }
+ }
+}
diff --git a/src/test/resources/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test/testMigration.zip b/src/test/resources/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test/testMigration.zip
new file mode 100644
index 0000000..ba51565
Binary files /dev/null and b/src/test/resources/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test/testMigration.zip differ