From 5154d57474407fb8e21e5bcd238c6db20666fca8 Mon Sep 17 00:00:00 2001 From: Gavin Date: Tue, 24 Oct 2023 17:25:54 -0700 Subject: [PATCH] Add API translation method for setting admin object metadata --- .../AlterAdminObjectMetadataParams.java | 65 +++++ .../kbase/workspace/ObjectMetadataUpdate.java | 163 +++++++++++++ .../kbase/WorkspaceServerMethods.java | 64 +++++ .../kbase/WorkspaceServerMethodsTest.java | 224 ++++++++++++++++++ 4 files changed, 516 insertions(+) create mode 100644 src/us/kbase/workspace/AlterAdminObjectMetadataParams.java create mode 100644 src/us/kbase/workspace/ObjectMetadataUpdate.java create mode 100644 src/us/kbase/workspace/test/kbase/WorkspaceServerMethodsTest.java diff --git a/src/us/kbase/workspace/AlterAdminObjectMetadataParams.java b/src/us/kbase/workspace/AlterAdminObjectMetadataParams.java new file mode 100644 index 00000000..2b248559 --- /dev/null +++ b/src/us/kbase/workspace/AlterAdminObjectMetadataParams.java @@ -0,0 +1,65 @@ + +package us.kbase.workspace; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Generated; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + + +/** + *

Original spec-file type: AlterAdminObjectMetadataParams

+ *
+ * Input parameters for the alter_admin_object_metadata method.
+ *         updates - the metadata updates to apply to the objects. If the same object is specified
+ *                 twice in the list, the update order is unspecified.
+ * 
+ * + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@Generated("com.googlecode.jsonschema2pojo") +@JsonPropertyOrder({ + "updates" +}) +public class AlterAdminObjectMetadataParams { + + @JsonProperty("updates") + private List updates; + private Map additionalProperties = new HashMap(); + + @JsonProperty("updates") + public List getUpdates() { + return updates; + } + + @JsonProperty("updates") + public void setUpdates(List updates) { + this.updates = updates; + } + + public AlterAdminObjectMetadataParams withUpdates(List updates) { + this.updates = updates; + return this; + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(String name, Object value) { + this.additionalProperties.put(name, value); + } + + @Override + public String toString() { + return ((((("AlterAdminObjectMetadataParams"+" [updates=")+ updates)+", additionalProperties=")+ additionalProperties)+"]"); + } + +} diff --git a/src/us/kbase/workspace/ObjectMetadataUpdate.java b/src/us/kbase/workspace/ObjectMetadataUpdate.java new file mode 100644 index 00000000..71fb6677 --- /dev/null +++ b/src/us/kbase/workspace/ObjectMetadataUpdate.java @@ -0,0 +1,163 @@ + +package us.kbase.workspace; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Generated; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + + +/** + *

Original spec-file type: ObjectMetadataUpdate

+ *
+ * An object metadata update specification.
+ *                 Required arguments:
+ *                 ObjectIdentity oi - the object to be altered
+ *                 One or both of the following arguments are required:
+ *                 usermeta new - metadata to assign to the workspace. Duplicate keys will
+ *                         be overwritten.
+ *                 list remove - these keys will be removed from the workspace
+ *                         metadata key/value pairs.
+ * 
+ * + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@Generated("com.googlecode.jsonschema2pojo") +@JsonPropertyOrder({ + "oi", + "new", + "remove" +}) +public class ObjectMetadataUpdate { + + /** + *

Original spec-file type: ObjectIdentity

+ *
+     * An object identifier.
+     *                 Select an object by either:
+     *                         One, and only one, of the numerical id or name of the workspace.
+     *                                 ws_id wsid - the numerical ID of the workspace.
+     *                                 ws_name workspace - the name of the workspace.
+     *                         AND
+     *                         One, and only one, of the numerical id or name of the object.
+     *                                 obj_id objid- the numerical ID of the object.
+     *                                 obj_name name - name of the object.
+     *                         OPTIONALLY
+     *                                 obj_ver ver - the version of the object.
+     *                 OR an object reference string:
+     *                         obj_ref ref - an object reference string.
+     * 
+ * + */ + @JsonProperty("oi") + private ObjectIdentity oi; + @JsonProperty("new") + private Map _new; + @JsonProperty("remove") + private List remove; + private Map additionalProperties = new HashMap(); + + /** + *

Original spec-file type: ObjectIdentity

+ *
+     * An object identifier.
+     *                 Select an object by either:
+     *                         One, and only one, of the numerical id or name of the workspace.
+     *                                 ws_id wsid - the numerical ID of the workspace.
+     *                                 ws_name workspace - the name of the workspace.
+     *                         AND
+     *                         One, and only one, of the numerical id or name of the object.
+     *                                 obj_id objid- the numerical ID of the object.
+     *                                 obj_name name - name of the object.
+     *                         OPTIONALLY
+     *                                 obj_ver ver - the version of the object.
+     *                 OR an object reference string:
+     *                         obj_ref ref - an object reference string.
+     * 
+ * + */ + @JsonProperty("oi") + public ObjectIdentity getOi() { + return oi; + } + + /** + *

Original spec-file type: ObjectIdentity

+ *
+     * An object identifier.
+     *                 Select an object by either:
+     *                         One, and only one, of the numerical id or name of the workspace.
+     *                                 ws_id wsid - the numerical ID of the workspace.
+     *                                 ws_name workspace - the name of the workspace.
+     *                         AND
+     *                         One, and only one, of the numerical id or name of the object.
+     *                                 obj_id objid- the numerical ID of the object.
+     *                                 obj_name name - name of the object.
+     *                         OPTIONALLY
+     *                                 obj_ver ver - the version of the object.
+     *                 OR an object reference string:
+     *                         obj_ref ref - an object reference string.
+     * 
+ * + */ + @JsonProperty("oi") + public void setOi(ObjectIdentity oi) { + this.oi = oi; + } + + public ObjectMetadataUpdate withOi(ObjectIdentity oi) { + this.oi = oi; + return this; + } + + @JsonProperty("new") + public Map getNew() { + return _new; + } + + @JsonProperty("new") + public void setNew(Map _new) { + this._new = _new; + } + + public ObjectMetadataUpdate withNew(Map _new) { + this._new = _new; + return this; + } + + @JsonProperty("remove") + public List getRemove() { + return remove; + } + + @JsonProperty("remove") + public void setRemove(List remove) { + this.remove = remove; + } + + public ObjectMetadataUpdate withRemove(List remove) { + this.remove = remove; + return this; + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(java.lang.String name, Object value) { + this.additionalProperties.put(name, value); + } + + @Override + public java.lang.String toString() { + return ((((((((("ObjectMetadataUpdate"+" [oi=")+ oi)+", _new=")+ _new)+", remove=")+ remove)+", additionalProperties=")+ additionalProperties)+"]"); + } + +} diff --git a/src/us/kbase/workspace/kbase/WorkspaceServerMethods.java b/src/us/kbase/workspace/kbase/WorkspaceServerMethods.java index 12b072b3..581522d4 100644 --- a/src/us/kbase/workspace/kbase/WorkspaceServerMethods.java +++ b/src/us/kbase/workspace/kbase/WorkspaceServerMethods.java @@ -1,5 +1,6 @@ package us.kbase.workspace.kbase; +import static java.util.Objects.requireNonNull; import static us.kbase.common.utils.ServiceUtils.checkAddlArgs; import static us.kbase.workspace.kbase.ArgUtils.checkLong; import static us.kbase.workspace.kbase.ArgUtils.chooseInstant; @@ -29,6 +30,9 @@ import java.util.Map; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import us.kbase.auth.AuthException; import us.kbase.auth.AuthToken; import us.kbase.auth.ConfigurableAuthService; @@ -42,6 +46,7 @@ import us.kbase.typedobj.idref.IdReferenceHandlerSetFactory; import us.kbase.typedobj.idref.IdReferenceHandlerSetFactoryBuilder; import us.kbase.typedobj.idref.IdReferencePermissionHandlerSet; +import us.kbase.workspace.AlterAdminObjectMetadataParams; import us.kbase.workspace.CreateWorkspaceParams; import us.kbase.workspace.GetObjectInfo3Params; import us.kbase.workspace.GetObjectInfo3Results; @@ -53,6 +58,7 @@ import us.kbase.workspace.ListWorkspaceInfoParams; import us.kbase.workspace.ObjectData; import us.kbase.workspace.ObjectIdentity; +import us.kbase.workspace.ObjectMetadataUpdate; import us.kbase.workspace.ObjectSaveData; import us.kbase.workspace.SaveObjectsParams; import us.kbase.workspace.SetGlobalPermissionsParams; @@ -61,11 +67,13 @@ import us.kbase.workspace.WorkspacePermissions; import us.kbase.workspace.database.DependencyStatus; import us.kbase.workspace.database.ListObjectsParameters; +import us.kbase.workspace.database.MetadataUpdate; import us.kbase.workspace.database.ObjectIDNoWSNoVer; import us.kbase.workspace.database.ObjectIdentifier; import us.kbase.workspace.database.ObjectInformation; import us.kbase.workspace.database.Permission; import us.kbase.workspace.database.RefLimit; +import us.kbase.workspace.database.ResolvedObjectID; import us.kbase.workspace.database.User; import us.kbase.workspace.database.UserWorkspaceIDs; import us.kbase.workspace.database.Workspace; @@ -105,6 +113,10 @@ public WorkspaceServerMethods( this.auth = auth; } + private static Logger getLogger() { + return LoggerFactory.getLogger(WorkspaceServerMethods.class); + } + /** Get the core workspace instance underlying this server -> core translation layer. * @return the workspace. */ @@ -640,4 +652,56 @@ Long, Map>> getObjectHistory( final ObjectIdentifier oi = processObjectIdentifier(object); return objInfoToTuple(ws.getObjectHistory(user, oi, asAdmin), true, false); } + + /** Set administrative metadata on an object. This method is reserved for full workspace + * administrators only and should not be exposed in a public API. + * @param params the method parameters. + * @throws NoSuchObjectException if one of the objects doesn't exist. + * @throws CorruptWorkspaceDBException if the workspace database is corrupt. + * @throws WorkspaceCommunicationException if a communication error occurs contacting the + * database. + * @throws InaccessibleObjectException if one of the objects is inaccessible. + */ + public void setAdminObjectMetadata(final AlterAdminObjectMetadataParams params) + // TODO CODE corrupt & comm exceptions should be unchecked, there's no recovery + // and it's not the user's fault + throws WorkspaceCommunicationException, InaccessibleObjectException, + CorruptWorkspaceDBException, NoSuchObjectException { + checkAddlArgs( + requireNonNull(params, "params").getAdditionalProperties(), params.getClass()); + if (params.getUpdates() == null || params.getUpdates().isEmpty()) { + throw new IllegalArgumentException("updates list cannot be empty"); + } + final Map update = new HashMap<>(); + final ListIterator iter = params.getUpdates().listIterator(); + while (iter.hasNext()) { + try { + final ObjectMetadataUpdate u = requireNonNull(iter.next(), + ObjectMetadataUpdate.class.getSimpleName() + " cannot be null"); + checkAddlArgs(u.getAdditionalProperties(), ObjectMetadataUpdate.class); + final MetadataUpdate mu = new MetadataUpdate( + new WorkspaceUserMetadata(u.getNew()), u.getRemove()); + if (!mu.hasUpdate()) { + throw new IllegalArgumentException("A metadata update is required"); + } + update.put(processObjectIdentifier(u.getOi()), mu); + } catch (NullPointerException | IllegalArgumentException | MetadataException e) { + // TODO CODE user caused exceptions should be checked & have custom classes + // in preparation for adding error codes. Will need to do this if + // methods are converted to a REST-like API so 400s and 500s can be + // distinguished + throw new IllegalArgumentException(String.format( + "Error processing update index %s: %s", + iter.previousIndex(), e.getMessage()), e); + } + } + final Map objs = ws.setAdminObjectMetadata(update); + for (final ResolvedObjectID r: objs.values()) { + getLogger().info("Object {}/{}/{}", + r.getWorkspaceIdentifier().getID(), + r.getId(), + r.getVersion() + ); + } + } } diff --git a/src/us/kbase/workspace/test/kbase/WorkspaceServerMethodsTest.java b/src/us/kbase/workspace/test/kbase/WorkspaceServerMethodsTest.java new file mode 100644 index 00000000..7d1ada25 --- /dev/null +++ b/src/us/kbase/workspace/test/kbase/WorkspaceServerMethodsTest.java @@ -0,0 +1,224 @@ +package us.kbase.workspace.test.kbase; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.google.common.collect.ImmutableMap; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import us.kbase.common.test.TestCommon; +import us.kbase.common.test.TestCommon.LogEvent; +import us.kbase.workspace.AlterAdminObjectMetadataParams; +import us.kbase.workspace.ObjectIdentity; +import us.kbase.workspace.ObjectMetadataUpdate; +import us.kbase.workspace.database.MetadataUpdate; +import us.kbase.workspace.database.ObjectIdentifier; +import us.kbase.workspace.database.ResolvedObjectID; +import us.kbase.workspace.database.ResolvedWorkspaceID; +import us.kbase.workspace.database.Workspace; +import us.kbase.workspace.database.WorkspaceUserMetadata; +import us.kbase.workspace.kbase.WorkspaceServerMethods; + +public class WorkspaceServerMethodsTest { + + private static List logEvents; + + @BeforeClass + public static void beforeClass() { + logEvents = TestCommon.setUpSLF4JTestLoggerAppender("us.kbase.workspace"); + } + + @Before + public void before() { + logEvents.clear(); + } + + @Test + public void setAdminObjectMetadata() throws Exception { + final Workspace ws = mock(Workspace.class); + + final WorkspaceServerMethods wsm = new WorkspaceServerMethods(ws, null, null); + final MetadataUpdate mu1 = new MetadataUpdate(null, Arrays.asList("foo")); + final MetadataUpdate mu2 = new MetadataUpdate(new WorkspaceUserMetadata( + ImmutableMap.of("baz", "bar")), null); + final MetadataUpdate mu3 = new MetadataUpdate(new WorkspaceUserMetadata( + ImmutableMap.of("one", "hump")), Arrays.asList("dromedary")); + + when(ws.setAdminObjectMetadata(ImmutableMap.of( + ObjectIdentifier.getBuilderFromRefPath("myws/96").build(), mu1, + ObjectIdentifier.getBuilderFromRefPath("8/myobj/1").build(), mu2, + ObjectIdentifier.getBuilderFromRefPath("myws/myobj").build(), mu3 + ))).thenReturn(ImmutableMap.of( + ObjectIdentifier.getBuilderFromRefPath("myws/96").build(), + new ResolvedObjectID(new ResolvedWorkspaceID(3, "myws", false, false), + 96, 2, "myobj2", false), + ObjectIdentifier.getBuilderFromRefPath("8/myobj/1").build(), + new ResolvedObjectID(new ResolvedWorkspaceID(8, "myws2", false, false), + 3, 1, "myobj", false), + ObjectIdentifier.getBuilderFromRefPath("myws/myobj").build(), + new ResolvedObjectID(new ResolvedWorkspaceID(3, "myws", false, false), + 254, 108, "myobj", false) + )); + wsm.setAdminObjectMetadata(new AlterAdminObjectMetadataParams() + .withUpdates(Arrays.asList( + new ObjectMetadataUpdate() // test wsname, objid + .withOi(new ObjectIdentity().withWorkspace("myws").withObjid(96L)) + .withRemove(Arrays.asList("foo")), + new ObjectMetadataUpdate() // test wsid, objname, ver + .withOi(new ObjectIdentity().withWsid(8L).withName("myobj") + .withVer(1L)) + .withNew(ImmutableMap.of("baz", "bar")), + new ObjectMetadataUpdate() // test ref + .withOi(new ObjectIdentity().withRef("myws/myobj")) + .withNew(ImmutableMap.of("one", "hump")) + .withRemove(Arrays.asList("dromedary")) + ))); + + TestCommon.assertLogEventsCorrect( + logEvents, + new LogEvent(Level.INFO, "Object 3/96/2", WorkspaceServerMethods.class), + new LogEvent(Level.INFO, "Object 8/3/1", WorkspaceServerMethods.class), + new LogEvent(Level.INFO, "Object 3/254/108", WorkspaceServerMethods.class) + ); + } + + @Test + public void setAdminObjectMetadataFailBadTopLevelParams() throws Exception { + final WorkspaceServerMethods wsm = new WorkspaceServerMethods( + mock(Workspace.class), null, null); + + setAdminObjectMetadataFail(wsm, null, new NullPointerException("params")); + final AlterAdminObjectMetadataParams p = new AlterAdminObjectMetadataParams(); + p.setAdditionalProperties("foo", "bar"); + setAdminObjectMetadataFail(wsm, p, new IllegalArgumentException( + "Unexpected arguments in AlterAdminObjectMetadataParams: foo")); + setAdminObjectMetadataFail(wsm, new AlterAdminObjectMetadataParams(), + new IllegalArgumentException("updates list cannot be empty")); + setAdminObjectMetadataFail(wsm, new AlterAdminObjectMetadataParams().withUpdates(null), + new IllegalArgumentException("updates list cannot be empty")); + setAdminObjectMetadataFail(wsm, new AlterAdminObjectMetadataParams() + .withUpdates(Collections.emptyList()), + new IllegalArgumentException("updates list cannot be empty")); + } + + @Test + public void setAdminObjectMetadataFailBadObjectIDs() throws Exception { + // test a selection of bad object identifiers, not meant to be exhaustive. + final ObjectIdentity good = new ObjectIdentity().withWsid(1L).withObjid(1L); + + setAdminObjectMetadataFailBadObjectID(new IllegalArgumentException( + "Error processing update index 1: ObjectIdentity cannot be null"), good, null); + setAdminObjectMetadataFailBadObjectID(new IllegalArgumentException( + "Error processing update index 0: Must provide one and only one of workspace name " + + "(was: null) or id (was: null)"), + new ObjectIdentity(), good); + setAdminObjectMetadataFailBadObjectID(new IllegalArgumentException( + "Error processing update index 2: Must provide one and only one of object name " + + "(was: n) or id (was: 1)"), + good, good, new ObjectIdentity().withWsid(1L).withName("n").withObjid(1L)); + setAdminObjectMetadataFailBadObjectID(new IllegalArgumentException( + "Error processing update index 3: Illegal number of separators '/' in " + + "object reference '1/2/3/4'"), + good, good, good, new ObjectIdentity().withRef("1/2/3/4")); + } + + private void setAdminObjectMetadataFailBadObjectID( + final Exception expected, + final ObjectIdentity... oi) { + final WorkspaceServerMethods wsm = new WorkspaceServerMethods( + mock(Workspace.class), null, null); + setAdminObjectMetadataFail( + wsm, + new AlterAdminObjectMetadataParams().withUpdates(Stream.of(oi) + .map(o -> new ObjectMetadataUpdate() + .withOi(o).withRemove(Arrays.asList("foo"))) + .collect(Collectors.toList())), + expected); + } + + @Test + public void setAdminObjectMetadataFailBadMetadata() throws Exception { + // test a selection of problems, again not exhaustive for all possible code paths + // through dependencies + final ObjectMetadataUpdate good = omu(Arrays.asList("foo")); + + setAdminObjectMetadataFailMetadata(new IllegalArgumentException( + "Error processing update index 1: ObjectMetadataUpdate cannot be null"), + good, null, good); + final ObjectMetadataUpdate addl = new ObjectMetadataUpdate(); + addl.setAdditionalProperties("yay", "boo"); + setAdminObjectMetadataFailMetadata(new IllegalArgumentException( + "Error processing update index 2: Unexpected arguments in " + + "ObjectMetadataUpdate: yay"), + good, good, addl); + setAdminObjectMetadataFailMetadata(new IllegalArgumentException( + "Error processing update index 1: null metadata keys are not allowed in the " + + "remove parameter"), + good, omu(Arrays.asList("foo", null))); + setAdminObjectMetadataFailMetadata(new IllegalArgumentException( + "Error processing update index 0: A metadata update is required"), + omu(), good); + final Map nully = new HashMap<>(); + nully.put("foo", null); + setAdminObjectMetadataFailMetadata(new IllegalArgumentException( + "Error processing update index 2: Null value for metadata key foo"), + good, good, omu(nully), good); + + + } + + private ObjectMetadataUpdate omu() { + return new ObjectMetadataUpdate().withOi(new ObjectIdentity().withRef("1/1")); + } + + private ObjectMetadataUpdate omu(final Map newm) { + return new ObjectMetadataUpdate() + .withOi(new ObjectIdentity().withRef("1/1")) + .withNew(newm); + } + + private ObjectMetadataUpdate omu(final List remove) { + return new ObjectMetadataUpdate() + .withOi(new ObjectIdentity().withRef("1/1")) + .withRemove(remove); + } + + private void setAdminObjectMetadataFailMetadata( + final Exception expected, + final ObjectMetadataUpdate ... omu) { + final WorkspaceServerMethods wsm = new WorkspaceServerMethods( + mock(Workspace.class), null, null); + setAdminObjectMetadataFail( + wsm, + new AlterAdminObjectMetadataParams().withUpdates(Arrays.asList(omu)), + expected); + } + + private void setAdminObjectMetadataFail( + final WorkspaceServerMethods wsm, + final AlterAdminObjectMetadataParams params, + final Exception expected) { + try { + wsm.setAdminObjectMetadata(params); + fail("expected exception"); + } catch (Exception got) { + TestCommon.assertExceptionCorrect(got, expected); + } + } + + +}