diff --git a/.gitignore b/.gitignore index 42281a30..6dc115d4 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,5 @@ local.properties .idea *.iml +.moquette_uuid +file_sme.json diff --git a/src/main/java/org/eclipse/basyx/extensions/internal/storage/BaSyxStorageAPI.java b/src/main/java/org/eclipse/basyx/extensions/internal/storage/BaSyxStorageAPI.java index 589c3e64..7b2330f6 100644 --- a/src/main/java/org/eclipse/basyx/extensions/internal/storage/BaSyxStorageAPI.java +++ b/src/main/java/org/eclipse/basyx/extensions/internal/storage/BaSyxStorageAPI.java @@ -26,11 +26,15 @@ import java.io.File; import java.io.InputStream; +import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; +import org.apache.commons.collections4.CollectionUtils; import org.eclipse.basyx.aas.metamodel.api.IAssetAdministrationShell; import org.eclipse.basyx.aas.metamodel.map.descriptor.AASDescriptor; +import org.eclipse.basyx.aas.metamodel.map.descriptor.ModelDescriptor; import org.eclipse.basyx.submodel.metamodel.api.ISubmodel; import org.eclipse.basyx.submodel.metamodel.api.qualifier.IIdentifiable; import org.eclipse.basyx.submodel.metamodel.api.submodelelement.ISubmodelElement; @@ -56,6 +60,13 @@ public abstract class BaSyxStorageAPI implements IBaSyxStorageAPI { protected final String COLLECTION_NAME; protected final Class TYPE; + /** + * The default constructor is primarily used for mocking purposes of the class. + */ + BaSyxStorageAPI() { + this(null, null); + } + /** * * @param collectionName @@ -70,9 +81,10 @@ public BaSyxStorageAPI(String collectionName, Class type) { } /** - * DISCLAIMER: Currently only supports to extract keys from IIdentifiables. - * Helper method that extracts a key for persistence storage requests from an - * object. + * DISCLAIMER: Currently only supports to extract keys from IIdentifiables and + * ModelDescriptors. + * Helper method that extracts a key for persistence storage + * requests from an object. * * @param obj * An object that contains a key that can be used to find the @@ -80,10 +92,16 @@ public BaSyxStorageAPI(String collectionName, Class type) { * @return The key */ protected String getKey(T obj) { - if (!(obj instanceof IIdentifiable)) { - throw new IllegalArgumentException("Can only extract a key from a object of type " + IIdentifiable.class.getName()); + if (!(obj instanceof IIdentifiable || obj instanceof ModelDescriptor)) { + throw new IllegalArgumentException("Can only extract a key from a object of types " + IIdentifiable.class.getName() + " or " + ModelDescriptor.class.getName()); + } + + if (obj instanceof ModelDescriptor) { + return ((ModelDescriptor) obj).getIdentifier().getId(); + } else { + return ((IIdentifiable) obj).getIdentification().getId(); + } - return ((IIdentifiable) obj).getIdentification().getId(); } /** @@ -100,12 +118,22 @@ protected String getKey(T obj) { */ public abstract T rawRetrieve(String key); + public abstract Collection rawRetrieveAll(); + public abstract File getFile(String key, String parentKey, Map objMap); public abstract String writeFile(String key, String parentKey, InputStream fileStream, ISubmodelElement submodelElement); public abstract void deleteFile(Submodel submodel, String idShort); + /** + * This Method shall return an implementation specific connection object which + * can be used to implement more advanced operations on the storage. + * + * @return An object that can be used for direct storage access + */ + public abstract Object getStorageConnection(); + /** * Returns a Object that was originally retrieved from the abstract method * {@code rawRetrieve}. If the object to be returned is a submodel type, it will @@ -121,6 +149,20 @@ public T retrieve(String key) { return retrieved; } + @SuppressWarnings("unchecked") + @Override + public Collection retrieveAll() { + Collection retrieves = rawRetrieveAll(); + if (!CollectionUtils.isEmpty(retrieves) && isSubmodelType(getElementClass(retrieves))) { + return (Collection) retrieves.stream().map(submodel -> handleRetrievedSubmodel((Submodel) submodel)).collect(Collectors.toList()); + } + return retrieves; + } + + private Class getElementClass(Collection collection) { + return collection.iterator().next().getClass(); + } + /** * Helper to bring the SubmodelElements a retrieved SubmodelObject to the * correct Type of ISubmodelElements. Background: SubmodelsElements are @@ -137,7 +179,7 @@ public T retrieve(String key) { @SuppressWarnings("unchecked") protected Submodel handleRetrievedSubmodel(Submodel retrieved) { Map> elementMaps = (Map>) retrieved.get(Submodel.SUBMODELELEMENT); - Map elements = forceToISubmodelElements(elementMaps); + Map elements = enforceISubmodelElements(elementMaps); retrieved.put(Submodel.SUBMODELELEMENT, elements); return retrieved; } @@ -154,7 +196,7 @@ protected Submodel handleRetrievedSubmodel(Submodel retrieved) { * can be get with {@code submodel.get(Submodel.SUBMODELELEMENT)})) * @return A map in the expected form of {@code Map} */ - private Map forceToISubmodelElements(Map> submodelElementObjectMap) { + private Map enforceISubmodelElements(Map> submodelElementObjectMap) { Map elements = new HashMap<>(); submodelElementObjectMap.forEach((idShort, elementMap) -> { @@ -164,32 +206,23 @@ private Map forceToISubmodelElements(Map type) { return ISubmodel.class.isAssignableFrom(type); } - /* - * Not yet tested - */ protected boolean isShellType(Class type) { return IAssetAdministrationShell.class.isAssignableFrom(type); } - /* - * Not yet tested - */ protected boolean isAASDescriptorType(Class type) { return AASDescriptor.class.isAssignableFrom(type); } - /* - * Not yet tested - */ protected boolean isBaSyxType(Class type) { return (isShellType(type) || isSubmodelType(type) || isAASDescriptorType(type)); } + public String getCollectionName() { + return COLLECTION_NAME; + } } diff --git a/src/main/java/org/eclipse/basyx/vab/exception/FeatureNotImplementedException.java b/src/main/java/org/eclipse/basyx/vab/exception/FeatureNotImplementedException.java index e6ff4a30..a322cd5b 100644 --- a/src/main/java/org/eclipse/basyx/vab/exception/FeatureNotImplementedException.java +++ b/src/main/java/org/eclipse/basyx/vab/exception/FeatureNotImplementedException.java @@ -37,4 +37,12 @@ public class FeatureNotImplementedException extends RuntimeException { */ private static final long serialVersionUID = 1L; + public FeatureNotImplementedException() { + super(); + } + + public FeatureNotImplementedException(final String message) { + super(message); + } + } diff --git a/src/test/java/org/eclipse/basyx/testsuite/regression/extensions/storage/BaSyxStorageAPISuite.java b/src/test/java/org/eclipse/basyx/testsuite/regression/extensions/storage/BaSyxStorageAPISuite.java index 424f17bf..c1922fae 100644 --- a/src/test/java/org/eclipse/basyx/testsuite/regression/extensions/storage/BaSyxStorageAPISuite.java +++ b/src/test/java/org/eclipse/basyx/testsuite/regression/extensions/storage/BaSyxStorageAPISuite.java @@ -28,6 +28,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.util.Arrays; import java.util.Collection; import org.eclipse.basyx.extensions.internal.storage.BaSyxStorageAPI; @@ -49,12 +50,22 @@ */ public abstract class BaSyxStorageAPISuite { - private static final Identifier SUBMODEL_IDENTIFIER = new Identifier(IdentifierType.CUSTOM, "testSubmodelIidentifier"); - protected static Submodel testType = new Submodel("testSubmodel", SUBMODEL_IDENTIFIER); - private BaSyxStorageAPI storageAPI; + private final Identifier SUBMODEL_IDENTIFIER = new Identifier(IdentifierType.CUSTOM, "testSubmodelIidentifier"); + protected Submodel testSubmodel = new Submodel("testSubmodel", SUBMODEL_IDENTIFIER); + protected BaSyxStorageAPI storageAPI; protected abstract BaSyxStorageAPI getStorageAPI(); + /** + * This Storage API is used to approve data persistency. Therefore this method + * shall not return the very same object returned by {@link #getStorageAPI()} + * but still connect to the same storage. + * + * @return A BaSyxStorageAPI that is not same as returned by + * {@link #getStorageAPI()} + */ + protected abstract BaSyxStorageAPI getSecondStorageAPI(); + @Before public void setUp() { storageAPI = getStorageAPI(); @@ -62,31 +73,80 @@ public void setUp() { @After public void cleanUp() { - storageAPI.delete(testType.getIdentification().getId()); + storageAPI.delete(testSubmodel.getIdentification().getId()); } @Test - public void retrieve() { - storageAPI.createOrUpdate(testType); - Submodel actual = storageAPI.retrieve(testType.getIdentification().getId()); - assertEquals(testType, actual); + public void createOrUpdateAndretrieve() { + storageAPI.createOrUpdate(testSubmodel); + Submodel actual = storageAPI.retrieve(testSubmodel.getIdentification().getId()); + assertEquals(testSubmodel, actual); + } + + @Test + public void retrieveAll() { + Submodel[] testSubmodels = createTestSubmodels(); + uploadMultiple(testSubmodels); + + Collection retrieves = storageAPI.retrieveAll(); + for (Submodel submodel : testSubmodels) { + assertTrue(retrieves.contains(submodel)); + } + } + + private Submodel[] createTestSubmodels() { + Submodel[] testTypes = new Submodel[3]; + Arrays.setAll(testTypes, i -> new Submodel(testSubmodel.getIdShort() + i, new Identifier(IdentifierType.CUSTOM, "test" + i))); + return testTypes; + } + + private void uploadMultiple(Submodel[] testTypes) { + for (Submodel submodel : testTypes) { + storageAPI.createOrUpdate(submodel); + } } @Test public void update() { - storageAPI.createOrUpdate(testType); - testType.setIdShort("updated"); - Submodel actual = storageAPI.createOrUpdate(testType); + storageAPI.createOrUpdate(testSubmodel); + testSubmodel.setIdShort("updated"); + Submodel actual = storageAPI.update(testSubmodel, testSubmodel.getIdentification().getId()); - assertEquals(testType, actual); + assertEquals(testSubmodel, actual); + } + + @Test + public void updateShallServeAsCreate() { + Submodel actual = storageAPI.update(testSubmodel, testSubmodel.getIdentification().getId()); + + assertEquals(testSubmodel, actual); } @Test public void delete() { - System.out.println(testType); - storageAPI.createOrUpdate(testType); - assertTrue(storageAPI.delete(testType.getIdentification().getId())); + storageAPI.createOrUpdate(testSubmodel); + assertTrue(storageAPI.delete(testSubmodel.getIdentification().getId())); Collection allElements = storageAPI.retrieveAll(); - assertFalse(allElements.contains(testType)); + assertFalse(allElements.contains(testSubmodel)); + } + + @Test + public void proofPersistency() { + storageAPI.createOrUpdate(testSubmodel); + Submodel persistentSubmodel = getSecondStorageAPI().retrieve(testSubmodel.getIdentification().getId()); + assertEquals(testSubmodel, persistentSubmodel); } + + /** + * This test must be implemented individually for every storage backend + */ + @Test + public abstract void createCollectionIfNotExists(); + + /** + * This test must be implemented individually for every storage backend + */ + @Test + public abstract void deleteCollection(); + } diff --git a/src/test/java/org/eclipse/basyx/testsuite/regression/extensions/storage/TestBaSyxStorageAPI.java b/src/test/java/org/eclipse/basyx/testsuite/regression/extensions/storage/TestBaSyxStorageAPI.java new file mode 100644 index 00000000..a9c8071d --- /dev/null +++ b/src/test/java/org/eclipse/basyx/testsuite/regression/extensions/storage/TestBaSyxStorageAPI.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (C) 2023 the Eclipse BaSyx Authors + * + * 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. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ +package org.eclipse.basyx.testsuite.regression.extensions.storage; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.basyx.extensions.internal.storage.BaSyxStorageAPI; +import org.eclipse.basyx.submodel.metamodel.map.Submodel; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; + +/** + * + * @author jungjan + * + */ +public class TestBaSyxStorageAPI extends BaSyxStorageAPISuite { + private Map mockedStorage = new HashMap<>(); + + @SuppressWarnings("unchecked") + private BaSyxStorageAPI mockedStorageAPI = Mockito.spy(BaSyxStorageAPI.class); + + public TestBaSyxStorageAPI() { + super(); + initMockedStorageAPI(); + } + + private void initMockedStorageAPI() { + Mockito.when(mockedStorageAPI.createOrUpdate(Mockito.any(Submodel.class))).then(this::mockedCreateOrUpdate); + Mockito.when(mockedStorageAPI.update(Mockito.any(Submodel.class), Mockito.anyString())).then(this::mockedUpdate); + Mockito.when(mockedStorageAPI.rawRetrieve(Mockito.anyString())).then(this::mockedRawRetrieve); + Mockito.when(mockedStorageAPI.rawRetrieveAll()).then(this::mockedRawRetrieveAll); + Mockito.when(mockedStorageAPI.delete(Mockito.anyString())).then(this::mockedDelete); + } + + @Override + protected BaSyxStorageAPI getStorageAPI() { + return mockedStorageAPI; + } + + /** + * Please DON'T implement this method as below for Production tests. This method + * shall help proof the data persistency. Therefore in production this method + * must return a BaSyxStorageAPI that is not the same as the one returned by + * {@link #getStorageAPI()} + */ + @Override + protected BaSyxStorageAPI getSecondStorageAPI() { + return getStorageAPI(); + } + + private Submodel mockedCreateOrUpdate(InvocationOnMock invocation) { + Submodel submodel = invocation.getArgument(0); + return createOrUpdate(submodel); + } + + private Submodel createOrUpdate(Submodel submodel) { + String identificationId = submodel.getIdentification().getId(); + mockedStorage.put(identificationId, submodel); + return submodel; + } + + private Submodel mockedUpdate(InvocationOnMock invocation) { + String key = invocation.getArgument(1); + Submodel submodel = invocation.getArgument(0); + if (!mockedStorageHasKey(key)) { + createOrUpdate(submodel); + } + mockedStorage.put(key, submodel); + return submodel; + } + + private Submodel mockedRawRetrieve(InvocationOnMock invocation) { + String key = invocation.getArgument(0); + if (!mockedStorageHasKey(key)) { + return null; + } + return mockedStorage.get(key); + } + + private Collection mockedRawRetrieveAll(InvocationOnMock invocation) { + return this.mockedStorage.values(); + } + + private boolean mockedDelete(InvocationOnMock invocation) { + String key = invocation.getArgument(0); + if (!mockedStorageHasKey(key)) { + return false; + } + mockedStorage.remove(key); + return true; + } + + private boolean mockedStorageHasKey(String identificationId) { + return mockedStorage.containsKey(identificationId); + } + + @Override + public void createCollectionIfNotExists() { + mockedStorageAPI.createCollectionIfNotExists("testCollection"); + Mockito.verify(mockedStorageAPI, Mockito.times(1)).createCollectionIfNotExists("testCollection"); + } + + @Override + public void deleteCollection() { + mockedStorageAPI.deleteCollection(); + Mockito.verify(mockedStorageAPI, Mockito.times(1)).deleteCollection(); + } + +} diff --git a/src/test/java/org/eclipse/basyx/testsuite/regression/submodel/restapi/SubmodelProviderTest.java b/src/test/java/org/eclipse/basyx/testsuite/regression/submodel/restapi/SubmodelProviderTest.java index a24e5cc5..bf0fa004 100644 --- a/src/test/java/org/eclipse/basyx/testsuite/regression/submodel/restapi/SubmodelProviderTest.java +++ b/src/test/java/org/eclipse/basyx/testsuite/regression/submodel/restapi/SubmodelProviderTest.java @@ -32,8 +32,8 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedHashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.Map; import org.eclipse.basyx.submodel.metamodel.api.submodelelement.ISubmodelElement; @@ -686,6 +686,26 @@ public void testInvokeAsyncException() throws Exception { } } + @SuppressWarnings("unchecked") + @Test + public void testIndirectSubmodelElementCollectionValueType() { + VABElementProxy submodelProvider = getConnectionManager().connectToVABElement(submodelAddr); + String containerRootPath = VABPathTools.concatenatePaths(SubmodelProvider.SUBMODEL, MultiSubmodelElementProvider.ELEMENTS, "containerRoot"); + Map directCollection = (Map) submodelProvider.getValue(containerRootPath); + Object indirectCollectionValue = directCollection.get(Property.VALUE); + + assertTrue(indirectCollectionValue instanceof Collection); + } + + @Test + public void testDirectSubmodelElementCollectionValueType() { + VABElementProxy submodelProvider = getConnectionManager().connectToVABElement(submodelAddr); + String containerRootValuePath = VABPathTools.concatenatePaths(SubmodelProvider.SUBMODEL, MultiSubmodelElementProvider.ELEMENTS, "containerRoot", Property.VALUE); + Object directCollectionValue = submodelProvider.getValue(containerRootValuePath); + + assertTrue(directCollectionValue instanceof Collection); + } + protected Map wrapParameter(String name, Object value) { Map param = new LinkedHashMap<>(); param.put(Identifiable.IDSHORT, name);