scripts;
+
+ /**
+ * @param previous The current {@link ScriptMetaData} or {@code null} if there
+ * is no existing {@link ScriptMetaData}.
+ */
+ public Builder(ScriptMetaData previous) {
+ this.scripts = previous == null ? new HashMap<>() :new HashMap<>(previous.scripts);
+ }
+
+ /**
+ * Add a new script to the existing stored scripts. The script will be added under
+ * both the new namespace and the deprecated namespace, so that look ups under
+ * the deprecated namespace will continue to work. Should a script already exist under
+ * the new namespace using a different language, it will be replaced and a deprecation
+ * warning will be issued. The replaced script will still exist under the deprecated
+ * namespace and can continue to be looked up this way until it is deleted.
+ *
+ * Take for example script 'A' with lang 'L0' and data 'D0'. If we add script 'A' to the
+ * empty set, the scripts {@link Map} will be ["A" -- D0, "A#L0" -- D0]. If a script
+ * 'A' with lang 'L1' and data 'D1' is then added, the scripts {@link Map} will be
+ * ["A" -- D1, "A#L1" -- D1, "A#L0" -- D0].
+ * @param id The user-specified id to use for the look up.
+ * @param source The user-specified stored script data held in {@link StoredScriptSource}.
+ */
+ public Builder storeScript(String id, StoredScriptSource source) {
+ StoredScriptSource previous = scripts.put(id, source);
+ scripts.put(source.getLang() + "#" + id, source);
+
+ if (previous != null && previous.getLang().equals(source.getLang()) == false) {
+ DEPRECATION_LOGGER.deprecated("stored script [" + id + "] already exists using a different lang " +
+ "[" + previous.getLang() + "], the new namespace for stored scripts will only use (id) instead of (lang, id)");
+ }
+
+ return this;
+ }
+
+ /**
+ * Delete a script from the existing stored scripts. The script will be removed from the
+ * new namespace if the script language matches the current script under the same id or
+ * if the script language is {@code null}. The script will be removed from the deprecated
+ * namespace on any delete either using using the specified lang parameter or the language
+ * found from looking up the script in the new namespace.
+ *
+ * Take for example a scripts {@link Map} with {"A" -- D1, "A#L1" -- D1, "A#L0" -- D0}.
+ * If a script is removed specified by an id 'A' and lang {@code null} then the scripts
+ * {@link Map} will be {"A#L0" -- D0}. To remove the final script, the deprecated
+ * namespace must be used, so an id 'A' and lang 'L0' would need to be specified.
+ * @param id The user-specified id to use for the look up.
+ * @param lang The user-specified language to use for the look up if using the deprecated
+ * namespace, otherwise {@code null}.
+ */
+ public Builder deleteScript(String id, String lang) {
+ StoredScriptSource source = scripts.get(id);
+
+ if (lang == null) {
+ if (source == null) {
+ throw new ResourceNotFoundException("stored script [" + id + "] does not exist and cannot be deleted");
+ }
+
+ lang = source.getLang();
+ }
+
+ if (source != null) {
+ if (lang.equals(source.getLang())) {
+ scripts.remove(id);
+ }
+ }
+
+ source = scripts.get(lang + "#" + id);
+
+ if (source == null) {
+ throw new ResourceNotFoundException(
+ "stored script [" + id + "] using lang [" + lang + "] does not exist and cannot be deleted");
+ }
+
+ scripts.remove(lang + "#" + id);
+
+ return this;
+ }
+
+ /**
+ * @return A {@link ScriptMetaData} with the updated {@link Map} of scripts.
+ */
+ public ScriptMetaData build() {
+ return new ScriptMetaData(scripts);
+ }
+ }
+
+ static final class ScriptMetadataDiff implements NamedDiff {
+
+ final Diff