diff --git a/radixdlt-java/src/main/java/com/radixdlt/client/application/RadixApplicationAPI.java b/radixdlt-java/src/main/java/com/radixdlt/client/application/RadixApplicationAPI.java index d595dd265..ce0adc000 100644 --- a/radixdlt-java/src/main/java/com/radixdlt/client/application/RadixApplicationAPI.java +++ b/radixdlt-java/src/main/java/com/radixdlt/client/application/RadixApplicationAPI.java @@ -42,9 +42,15 @@ import com.radixdlt.client.application.translate.StatefulActionToParticleGroupsMapper; import com.radixdlt.client.application.translate.StatelessActionToParticleGroupsMapper; import com.radixdlt.client.application.translate.data.AtomToDecryptedMessageMapper; +import com.radixdlt.client.application.translate.data.CRUDataUpdate; +import com.radixdlt.client.application.translate.data.AtomToCRUDataUpdateMapper; +import com.radixdlt.client.application.translate.data.CreateCRUDataAction; +import com.radixdlt.client.application.translate.data.CreateDataToParticleGroupsMapper; import com.radixdlt.client.application.translate.data.DecryptedMessage; import com.radixdlt.client.application.translate.data.SendMessageAction; import com.radixdlt.client.application.translate.data.SendMessageToParticleGroupsMapper; +import com.radixdlt.client.application.translate.data.UpdateCRUDataAction; +import com.radixdlt.client.application.translate.data.UpdateCRUDataToParticleGroupsMapper; import com.radixdlt.client.application.translate.tokens.AtomToTokenTransfersMapper; import com.radixdlt.client.application.translate.tokens.BurnTokensAction; import com.radixdlt.client.application.translate.tokens.BurnTokensActionMapper; @@ -146,13 +152,16 @@ public static RadixApplicationAPIBuilder defaultBuilder() { SendMessageAction.class, new SendMessageToParticleGroupsMapper(ECKeyPair::generateNew) ) + .addStatelessParticlesMapper(CreateCRUDataAction.class, new CreateDataToParticleGroupsMapper()) .addStatelessParticlesMapper(CreateTokenAction.class, new CreateTokenToParticleGroupsMapper()) .addStatelessParticlesMapper(PutUniqueIdAction.class, new PutUniqueIdToParticleGroupsMapper()) + .addStatefulParticlesMapper(UpdateCRUDataAction.class, new UpdateCRUDataToParticleGroupsMapper()) .addStatefulParticlesMapper(MintTokensAction.class, new MintTokensActionMapper()) .addStatefulParticlesMapper(BurnTokensAction.class, new BurnTokensActionMapper()) .addStatefulParticlesMapper(TransferTokensAction.class, new TransferTokensToParticleGroupsMapper()) .addReducer(new TokenDefinitionsReducer()) .addReducer(new TokenBalanceReducer()) + .addAtomMapper(new AtomToCRUDataUpdateMapper()) .addAtomMapper(new AtomToDecryptedMessageMapper()) .addAtomMapper(new AtomToTokenTransfersMapper()) .addAtomErrorMapper(new AlreadyUsedUniqueIdReasonMapper()); @@ -435,6 +444,50 @@ public Result sendMessage(RadixAddress toAddress, byte[] data, boolean encrypt) return execute(sendMessageAction); } + /** + * Returns a never ending stream of data stored at a given address. The pull() + * method must be called to continually retrieve the latest messages. + * + * @param address the address to retrieve data from + * @return a cold observable of the messages at the given address + */ + public Observable observeData(RadixAddress address) { + Objects.requireNonNull(address); + return observeActions(CRUDataUpdate.class, address); + } + + /** + * Creates a data resource and stores it. + * + * @param rri the resource which will be associated with this data + * @param data to be storedg + * @return result of the create data action + */ + public Result createData(RRI rri, byte[] data) { + RadixAddress address = getAddress(); + if (!rri.getAddress().equals(address)) { + throw new IllegalArgumentException(); + } + CreateCRUDataAction createDataAction = CreateCRUDataAction.create(rri, data); + return execute(createDataAction); + } + + /** + * Update an existing data resource + * + * @param rri the resource which is associated with this data + * @param data new version of data + * @return result of the send message execution + */ + public Result updateData(RRI rri, byte[] data) { + RadixAddress address = getAddress(); + if (!rri.getAddress().equals(address)) { + throw new IllegalArgumentException(); + } + UpdateCRUDataAction updateAction = UpdateCRUDataAction.create(rri, data); + return execute(updateAction); + } + /** * Returns a never ending stream of token transfers stored at the current address. * pull() must be called to continually retrieve the latest transfers. diff --git a/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/AtomToCRUDataUpdateMapper.java b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/AtomToCRUDataUpdateMapper.java new file mode 100644 index 000000000..49f2d7de5 --- /dev/null +++ b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/AtomToCRUDataUpdateMapper.java @@ -0,0 +1,58 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * 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 com.radixdlt.client.application.translate.data; + +import java.util.List; +import java.util.stream.Collectors; + +import com.radixdlt.client.application.identity.RadixIdentity; +import com.radixdlt.client.application.translate.AtomToExecutedActionsMapper; +import com.radixdlt.client.atommodel.cru.CRUDataParticle; +import com.radixdlt.client.core.atoms.Atom; +import com.radixdlt.client.core.atoms.particles.SpunParticle; + +import io.reactivex.Observable; + +/** + * Maps an atom to some number of CRUDataUpdate + */ +public class AtomToCRUDataUpdateMapper implements AtomToExecutedActionsMapper { + + @Override + public Class actionClass() { + return CRUDataUpdate.class; + } + + @Override + public Observable map(Atom atom, RadixIdentity identity) { + long timestamp = atom.getTimestamp(); + List dataUpdates = atom.spunParticles() + .map(SpunParticle::getParticle) + .filter(p -> p instanceof CRUDataParticle) + .map(CRUDataParticle.class::cast) + .map(p -> new CRUDataUpdate(p.getRRI(), p.data(), timestamp, p.euid())) + .collect(Collectors.toList()); + return Observable.fromIterable(dataUpdates); + } + +} diff --git a/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CRUDataMissingException.java b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CRUDataMissingException.java new file mode 100644 index 000000000..a7639e412 --- /dev/null +++ b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CRUDataMissingException.java @@ -0,0 +1,61 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * 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 com.radixdlt.client.application.translate.data; + +import com.radixdlt.client.application.translate.StageActionException; +import com.radixdlt.identifiers.RRI; +import java.util.Objects; + +public class CRUDataMissingException extends StageActionException { + private final RRI rri; + private final int nRecords; + + public CRUDataMissingException(RRI cruDataRri, int nRecords) { + super("Could not find CRU data " + cruDataRri + " to update"); + this.rri = cruDataRri; + this.nRecords = nRecords; + } + + public RRI rri() { + return this.rri; + } + + public int nRecords() { + return this.nRecords; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof CRUDataMissingException)) { + return false; + } + + CRUDataMissingException that = (CRUDataMissingException) obj; + return this.nRecords == that.nRecords && Objects.equals(this.rri, that.rri); + } + + @Override + public int hashCode() { + return this.nRecords * 31 + Objects.hashCode(this.rri); + } +} diff --git a/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CRUDataUpdate.java b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CRUDataUpdate.java new file mode 100644 index 000000000..9e2d3ad16 --- /dev/null +++ b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CRUDataUpdate.java @@ -0,0 +1,109 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * 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 com.radixdlt.client.application.translate.data; + + +import java.util.Arrays; +import java.util.Objects; + +import com.radixdlt.identifiers.EUID; +import com.radixdlt.identifiers.RRI; +import com.radixdlt.utils.Bytes; + +/** + * An application layer object representing some data found on the ledger. + */ +public class CRUDataUpdate { + + private final RRI rri; + private final byte[] data; + private final EUID actionId; + private final transient long timestamp; + + public CRUDataUpdate(RRI rri, byte[] data, long timestamp, EUID actionId) { + this.rri = rri; + this.data = data; + this.timestamp = timestamp; + this.actionId = actionId; + } + + /** + * The unique id for the this update action. + * @return {@link EUID} for the action + */ + public EUID getActionId() { + return this.actionId; + } + + /** + * The data from this update action. + * @return the data for the action + */ + public byte[] getData() { + return data; + } + + /** + * The timestamp from this update action. + *

+ * Timestamp is in milliseconds since Unix epoch. + * + * @return the timestamp for the action + */ + public long getTimestamp() { + return timestamp; + } + + /** + * The resource identifier for this update action. + * @return the resource identifier for the action + */ + public RRI rri() { + return rri; + } + + @Override + public int hashCode() { + return Objects.hash(this.actionId, this.rri) + * 31 + Arrays.hashCode(this.data); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof CRUDataUpdate) { + CRUDataUpdate that = (CRUDataUpdate) obj; + return Objects.equals(this.actionId, that.actionId) + && Objects.equals(this.rri, that.rri) + && Arrays.equals(this.data, that.data); + } + return false; + } + + @Override + public String toString() { + return timestamp + " " + rri + " " + Bytes.toHexString(data); + } +} diff --git a/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CreateCRUDataAction.java b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CreateCRUDataAction.java new file mode 100644 index 000000000..2b0c0b3f4 --- /dev/null +++ b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CreateCRUDataAction.java @@ -0,0 +1,60 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * 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 com.radixdlt.client.application.translate.data; + +import java.util.Objects; + +import com.radixdlt.client.application.translate.Action; +import com.radixdlt.identifiers.RRI; +import com.radixdlt.utils.Bytes; + +/** + * An Action object which create a CRU data object. + */ +public final class CreateCRUDataAction implements Action { + private final RRI rri; + private final byte[] data; + + private CreateCRUDataAction(RRI rri, byte[] data) { + this.rri = rri; + this.data = Objects.requireNonNull(data); + } + + public static CreateCRUDataAction create(RRI rri, byte[] data) { + return new CreateCRUDataAction(rri, data); + } + + public byte[] getData() { + return this.data; + } + + public RRI getRRI() { + return this.rri; + } + + @Override + public String toString() { + String dataString = this.data == null ? "null" : Bytes.toBase64String(this.data); + return String.format("%s[%s:%s]", getClass().getSimpleName(), this.rri, dataString); + } +} diff --git a/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CreateDataToParticleGroupsMapper.java b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CreateDataToParticleGroupsMapper.java new file mode 100644 index 000000000..dca6c20ce --- /dev/null +++ b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/CreateDataToParticleGroupsMapper.java @@ -0,0 +1,54 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * 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 com.radixdlt.client.application.translate.data; + +import java.util.Collections; +import java.util.List; + +import com.radixdlt.client.application.translate.StatelessActionToParticleGroupsMapper; +import com.radixdlt.client.atommodel.cru.CRUDataParticle; +import com.radixdlt.client.atommodel.rri.RRIParticle; +import com.radixdlt.client.core.atoms.ParticleGroup; +import com.radixdlt.client.core.atoms.particles.SpunParticle; + +/** + * Maps a create CRU data action to the particles necessary to be included in an atom. + */ +public class CreateDataToParticleGroupsMapper implements StatelessActionToParticleGroupsMapper { + + private static final int INITIAL_VERSION = 0; + + /** + * Create {@link ParticleGroup} objects representing the specified user action. + * + * @param action the action to mapToParticles to particles + * @return observable of particle groups to be included in an atom for a given action + */ + @Override + public List mapToParticleGroups(CreateCRUDataAction action) { + RRIParticle rriParticle = new RRIParticle(action.getRRI()); + CRUDataParticle cruParticule = new CRUDataParticle(action.getRRI(), INITIAL_VERSION, action.getData()); + ParticleGroup particleGroup = ParticleGroup.of(SpunParticle.down(rriParticle), SpunParticle.up(cruParticule)); + return Collections.singletonList(particleGroup); + } +} diff --git a/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/UpdateCRUDataAction.java b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/UpdateCRUDataAction.java new file mode 100644 index 000000000..63eb24145 --- /dev/null +++ b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/UpdateCRUDataAction.java @@ -0,0 +1,60 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * 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 com.radixdlt.client.application.translate.data; + +import java.util.Objects; + +import com.radixdlt.client.application.translate.Action; +import com.radixdlt.identifiers.RRI; +import com.radixdlt.utils.Bytes; + +/** + * An Action object which updates a CRU data object. + */ +public final class UpdateCRUDataAction implements Action { + private final RRI rri; + private final byte[] data; + + private UpdateCRUDataAction(RRI rri, byte[] data) { + this.rri = rri; + this.data = Objects.requireNonNull(data); + } + + public static UpdateCRUDataAction create(RRI rri, byte[] data) { + return new UpdateCRUDataAction(rri, data); + } + + public byte[] getData() { + return this.data; + } + + public RRI getRRI() { + return this.rri; + } + + @Override + public String toString() { + String dataString = this.data == null ? "null" : Bytes.toBase64String(this.data); + return String.format("%s[%s:%s]", getClass().getSimpleName(), this.rri, dataString); + } +} diff --git a/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/UpdateCRUDataToParticleGroupsMapper.java b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/UpdateCRUDataToParticleGroupsMapper.java new file mode 100644 index 000000000..aaeba627a --- /dev/null +++ b/radixdlt-java/src/main/java/com/radixdlt/client/application/translate/data/UpdateCRUDataToParticleGroupsMapper.java @@ -0,0 +1,70 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * 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 com.radixdlt.client.application.translate.data; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.common.collect.ImmutableSet; +import com.radixdlt.client.application.translate.ShardedParticleStateId; +import com.radixdlt.client.application.translate.StatefulActionToParticleGroupsMapper; +import com.radixdlt.client.atommodel.cru.CRUDataParticle; +import com.radixdlt.client.core.atoms.ParticleGroup; +import com.radixdlt.client.core.atoms.particles.Particle; +import com.radixdlt.client.core.atoms.particles.SpunParticle; +import com.radixdlt.identifiers.RRI; +import com.radixdlt.identifiers.RadixAddress; + +/** + * Creates {@link ParticleGroup} objects for specified CRU data update. + */ +public class UpdateCRUDataToParticleGroupsMapper implements StatefulActionToParticleGroupsMapper { + + @Override + public Set requiredState(UpdateCRUDataAction updateDataAction) { + RadixAddress address = updateDataAction.getRRI().getAddress(); + return ImmutableSet.of(ShardedParticleStateId.of(CRUDataParticle.class, address)); + } + + @Override + public List mapToParticleGroups(UpdateCRUDataAction updateDataAction, Stream store) throws CRUDataMissingException { + RRI rri = updateDataAction.getRRI(); + List particles = store.collect(Collectors.toList()); + List records = particles.stream() + .filter(p -> p instanceof CRUDataParticle) + .map(CRUDataParticle.class::cast) + .filter(p -> p.getRRI().equals(rri)) + .collect(Collectors.toList()); + if (records.size() != 1) { + // Serious problem if records is not empty, as this means that there is more than one CRU Data particle + assert records.isEmpty(); + throw new CRUDataMissingException(rri, records.size()); + } + CRUDataParticle prevData = records.get(0); + CRUDataParticle newData = new CRUDataParticle(rri, prevData.serialno() + 1, updateDataAction.getData()); + ParticleGroup particleGroup = ParticleGroup.of(SpunParticle.down(prevData), SpunParticle.up(newData)); + return Collections.singletonList(particleGroup); + } +} diff --git a/radixdlt-java/src/main/java/com/radixdlt/client/atommodel/cru/CRUDataParticle.java b/radixdlt-java/src/main/java/com/radixdlt/client/atommodel/cru/CRUDataParticle.java new file mode 100644 index 000000000..8eb5f935d --- /dev/null +++ b/radixdlt-java/src/main/java/com/radixdlt/client/atommodel/cru/CRUDataParticle.java @@ -0,0 +1,93 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ +package com.radixdlt.client.atommodel.cru; + +import javax.annotation.concurrent.Immutable; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.radixdlt.client.atommodel.Identifiable; +import com.radixdlt.client.core.atoms.particles.Particle; +import com.radixdlt.identifiers.RRI; +import com.radixdlt.serialization.DsonOutput; +import com.radixdlt.serialization.DsonOutput.Output; +import com.radixdlt.serialization.SerializerId2; +import com.radixdlt.utils.Bytes; + +/** + * Particle representing data that can be created, read and updated. + */ +@Immutable +@SerializerId2("radix.particles.cru") +public final class CRUDataParticle extends Particle implements Identifiable { + + // Account and lookup key + @JsonProperty("rri") + @DsonOutput(Output.ALL) + private RRI rri; + + // Serial / version number for updates + // First creation must == 0, updates incrementing monotonically + @JsonProperty("serialno") + @DsonOutput(Output.ALL) + private long serialno; + + // The actual data + @JsonProperty("data") + @DsonOutput(Output.ALL) + private byte[] data; + + CRUDataParticle() { + super(); + } + + public CRUDataParticle(RRI rri, long serialno, byte[] data) { + super(rri.getAddress().euid()); + + this.rri = rri; + this.serialno = serialno; + this.data = data; + } + + @Override + public RRI getRRI() { + return this.rri; + } + + /** + * Retrieve the serial number of this particle. + * @return the serial number. + */ + public long serialno() { + return this.serialno; + } + + /** + * Retrieve the data for this particle. + *

+ * Note that callers should not modify the contents of the returned array. + * @return the particle data. + */ + public byte[] data() { + return this.data; + } + + @Override + public String toString() { + String stringData = this.data == null ? "" : Bytes.toHexString(this.data); + return String.format("%s[(%s:%s), (%s)]", getClass().getSimpleName(), String.valueOf(this.rri), this.serialno, stringData); + } +} diff --git a/radixdlt-java/src/main/java/com/radixdlt/client/serialization/Serialize.java b/radixdlt-java/src/main/java/com/radixdlt/client/serialization/Serialize.java index 22ebe9c06..d6196160e 100644 --- a/radixdlt-java/src/main/java/com/radixdlt/client/serialization/Serialize.java +++ b/radixdlt-java/src/main/java/com/radixdlt/client/serialization/Serialize.java @@ -31,7 +31,7 @@ import com.radixdlt.client.atommodel.tokens.FixedSupplyTokenDefinitionParticle; import com.radixdlt.client.atommodel.tokens.TransferrableTokensParticle; - +import com.radixdlt.client.atommodel.cru.CRUDataParticle; import com.radixdlt.client.atommodel.message.MessageParticle; import com.radixdlt.client.atommodel.tokens.MutableSupplyTokenDefinitionParticle; import com.radixdlt.client.atommodel.unique.UniqueParticle; @@ -66,6 +66,7 @@ private static Collection> getClasses() { return Arrays.asList( Atom.class, AtomEvent.class, + CRUDataParticle.class, ParticleGroup.class, Particle.class, RRIParticle.class, diff --git a/radixdlt-java/src/test/java/com/radixdlt/client/application/translate/data/CRUDataUpdateTest.java b/radixdlt-java/src/test/java/com/radixdlt/client/application/translate/data/CRUDataUpdateTest.java new file mode 100644 index 000000000..d8516b2f7 --- /dev/null +++ b/radixdlt-java/src/test/java/com/radixdlt/client/application/translate/data/CRUDataUpdateTest.java @@ -0,0 +1,91 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * 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 com.radixdlt.client.application.translate.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.Test; + +import com.google.common.collect.ImmutableList; +import com.radixdlt.client.application.translate.ShardedParticleStateId; +import com.radixdlt.client.atommodel.cru.CRUDataParticle; +import com.radixdlt.client.atommodel.rri.RRIParticle; +import com.radixdlt.client.core.atoms.ParticleGroup; +import com.radixdlt.client.core.atoms.particles.Particle; +import com.radixdlt.client.core.atoms.particles.SpunParticle; +import com.radixdlt.identifiers.RRI; +import com.radixdlt.identifiers.RadixAddress; + +public class CRUDataUpdateTest { + @Test + public void testCreateMapper() { + RadixAddress address = RadixAddress.from("JEbhKQzBn4qJzWJFBbaPioA2GTeaQhuUjYWkanTE6N8VvvPpvM8"); + RRI rri = RRI.of(address, "TestData"); + byte[] data = new byte[128]; + Arrays.fill(data, (byte) 0xFE); + CreateCRUDataAction createDataAction = CreateCRUDataAction.create(rri, data); + CreateDataToParticleGroupsMapper mapper = new CreateDataToParticleGroupsMapper(); + List particleGroups = mapper.mapToParticleGroups(createDataAction); + assertThat(particleGroups).hasSize(1); + ImmutableList spunParticles = particleGroups.get(0).getSpunParticles(); + assertThat(spunParticles.get(0).getParticle()).isInstanceOf(RRIParticle.class); + assertThat(spunParticles.get(1).getParticle()).isInstanceOf(CRUDataParticle.class); + } + + @Test + public void testUpdateMapper() { + RadixAddress address = RadixAddress.from("JEbhKQzBn4qJzWJFBbaPioA2GTeaQhuUjYWkanTE6N8VvvPpvM8"); + RRI rri = RRI.of(address, "TestData"); + byte[] data = new byte[128]; + Arrays.fill(data, (byte) 0xFE); + UpdateCRUDataAction updateDataAction = UpdateCRUDataAction.create(rri, data); + UpdateCRUDataToParticleGroupsMapper mapper = new UpdateCRUDataToParticleGroupsMapper(); + Set state = mapper.requiredState(updateDataAction); + assertThat(state).hasSize(1); + } + + @Test + public void testUpdateAction() { + RadixAddress address = RadixAddress.from("JEbhKQzBn4qJzWJFBbaPioA2GTeaQhuUjYWkanTE6N8VvvPpvM8"); + RRI rri = RRI.of(address, "TestData"); + byte[] data = new byte[128]; + Arrays.fill(data, (byte) 0xFE); + UpdateCRUDataAction updateDataAction = UpdateCRUDataAction.create(rri, data); + List items = new ArrayList<>(); + CRUDataParticle oldData = new CRUDataParticle(rri, 0, new byte[64]); + items.add(oldData); + Stream store = items.stream(); + UpdateCRUDataToParticleGroupsMapper mapper = new UpdateCRUDataToParticleGroupsMapper(); + List groups = mapper.mapToParticleGroups(updateDataAction, store); + assertThat(groups).hasSize(1); + ImmutableList spunParticles = groups.get(0).getSpunParticles(); + assertThat(spunParticles.get(0).getParticle()).isInstanceOf(CRUDataParticle.class); + assertThat(spunParticles.get(1).getParticle()).isInstanceOf(CRUDataParticle.class); + } +}