forked from Qortal/qortal
JOIN Group + improved specific group info from API
This commit is contained in:
parent
83abede8ab
commit
760cb6cd37
@ -110,7 +110,10 @@ public enum ApiError {
|
||||
MESSAGE_FORMAT_NOT_HEX(1001, 400),
|
||||
MESSAGE_BLANK(1002, 400),
|
||||
NO_PUBLIC_KEY(1003, 422),
|
||||
MESSAGESIZE_EXCEEDED(1004, 400);
|
||||
MESSAGESIZE_EXCEEDED(1004, 400),
|
||||
|
||||
// Groups
|
||||
GROUP_UNKNOWN(1101, 404);
|
||||
|
||||
private final static Map<Integer, ApiError> map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError));
|
||||
|
||||
|
40
src/main/java/org/qora/api/model/GroupWithMemberInfo.java
Normal file
40
src/main/java/org/qora/api/model/GroupWithMemberInfo.java
Normal file
@ -0,0 +1,40 @@
|
||||
package org.qora.api.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
|
||||
import org.qora.data.group.GroupAdminData;
|
||||
import org.qora.data.group.GroupData;
|
||||
import org.qora.data.group.GroupMemberData;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "Group info, maybe including members")
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class GroupWithMemberInfo {
|
||||
|
||||
@Schema(implementation = GroupData.class, name = "group", title = "group info")
|
||||
@XmlElement(name = "group")
|
||||
public GroupData groupData;
|
||||
|
||||
Integer memberCount;
|
||||
|
||||
public List<GroupAdminData> groupAdmins;
|
||||
public List<GroupMemberData> groupMembers;
|
||||
|
||||
// For JAX-RS
|
||||
protected GroupWithMemberInfo() {
|
||||
}
|
||||
|
||||
public GroupWithMemberInfo(GroupData groupData, List<GroupAdminData> groupAdmins, List<GroupMemberData> groupMembers, Integer memberCount) {
|
||||
this.groupData = groupData;
|
||||
this.groupAdmins = groupAdmins;
|
||||
this.groupMembers = groupMembers;
|
||||
this.memberCount = memberCount;
|
||||
}
|
||||
|
||||
}
|
@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
@ -24,9 +25,13 @@ import javax.ws.rs.core.MediaType;
|
||||
import org.qora.api.ApiError;
|
||||
import org.qora.api.ApiErrors;
|
||||
import org.qora.api.ApiExceptionFactory;
|
||||
import org.qora.api.model.GroupWithMemberInfo;
|
||||
import org.qora.crypto.Crypto;
|
||||
import org.qora.data.group.GroupAdminData;
|
||||
import org.qora.data.group.GroupData;
|
||||
import org.qora.data.group.GroupMemberData;
|
||||
import org.qora.data.transaction.CreateGroupTransactionData;
|
||||
import org.qora.data.transaction.JoinGroupTransactionData;
|
||||
import org.qora.data.transaction.UpdateGroupTransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
@ -35,6 +40,7 @@ import org.qora.transaction.Transaction;
|
||||
import org.qora.transaction.Transaction.ValidationResult;
|
||||
import org.qora.transform.TransformationException;
|
||||
import org.qora.transform.transaction.CreateGroupTransactionTransformer;
|
||||
import org.qora.transform.transaction.JoinGroupTransactionTransformer;
|
||||
import org.qora.transform.transaction.UpdateGroupTransactionTransformer;
|
||||
import org.qora.utils.Base58;
|
||||
|
||||
@ -112,15 +118,40 @@ public class GroupsResource {
|
||||
description = "group info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(implementation = GroupData.class)
|
||||
schema = @Schema(implementation = GroupWithMemberInfo.class)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public GroupData getGroup(@PathParam("groupname") String groupName) {
|
||||
public GroupWithMemberInfo getGroup(@PathParam("groupname") String groupName, @QueryParam("includeMembers") boolean includeMembers) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getGroupRepository().fromGroupName(groupName);
|
||||
GroupData groupData = repository.getGroupRepository().fromGroupName(groupName);
|
||||
if (groupData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.GROUP_UNKNOWN);
|
||||
|
||||
List<GroupMemberData> groupMembers = null;
|
||||
Integer memberCount = null;
|
||||
|
||||
if (includeMembers) {
|
||||
groupMembers = repository.getGroupRepository().getAllGroupMembers(groupData.getGroupName());
|
||||
|
||||
// Strip groupName from member info
|
||||
groupMembers = groupMembers.stream().map(groupMemberData -> new GroupMemberData(null, groupMemberData.getMember(), groupMemberData.getJoined())).collect(Collectors.toList());
|
||||
|
||||
memberCount = groupMembers.size();
|
||||
} else {
|
||||
// Just count members instead
|
||||
memberCount = repository.getGroupRepository().countGroupMembers(groupData.getGroupName());
|
||||
}
|
||||
|
||||
// Always include admins
|
||||
List<GroupAdminData> groupAdmins = repository.getGroupRepository().getAllGroupAdmins(groupData.getGroupName());
|
||||
|
||||
// Strip groupName from admin info
|
||||
groupAdmins = groupAdmins.stream().map(groupAdminData -> new GroupAdminData(null, groupAdminData.getAdmin())).collect(Collectors.toList());
|
||||
|
||||
return new GroupWithMemberInfo(groupData, groupAdmins, groupMembers, memberCount);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@ -213,4 +244,47 @@ public class GroupsResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/join")
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, JOIN_GROUP transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = JoinGroupTransactionData.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, JOIN_GROUP transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String joinGroup(JoinGroupTransactionData transactionData) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
ValidationResult result = transaction.isValidUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
byte[] bytes = JoinGroupTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
35
src/main/java/org/qora/data/group/GroupAdminData.java
Normal file
35
src/main/java/org/qora/data/group/GroupAdminData.java
Normal file
@ -0,0 +1,35 @@
|
||||
package org.qora.data.group;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class GroupAdminData {
|
||||
|
||||
// Properties
|
||||
private String groupName;
|
||||
private String admin;
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAX-RS serialization
|
||||
protected GroupAdminData() {
|
||||
}
|
||||
|
||||
public GroupAdminData(String groupName, String admin) {
|
||||
this.groupName = groupName;
|
||||
this.admin = admin;
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
|
||||
public String getGroupName() {
|
||||
return this.groupName;
|
||||
}
|
||||
|
||||
public String getAdmin() {
|
||||
return this.admin;
|
||||
}
|
||||
|
||||
}
|
41
src/main/java/org/qora/data/group/GroupMemberData.java
Normal file
41
src/main/java/org/qora/data/group/GroupMemberData.java
Normal file
@ -0,0 +1,41 @@
|
||||
package org.qora.data.group;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class GroupMemberData {
|
||||
|
||||
// Properties
|
||||
private String groupName;
|
||||
private String member;
|
||||
private long joined;
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAX-RS serialization
|
||||
protected GroupMemberData() {
|
||||
}
|
||||
|
||||
public GroupMemberData(String groupName, String member, long joined) {
|
||||
this.groupName = groupName;
|
||||
this.member = member;
|
||||
this.joined = joined;
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
|
||||
public String getGroupName() {
|
||||
return this.groupName;
|
||||
}
|
||||
|
||||
public String getMember() {
|
||||
return this.member;
|
||||
}
|
||||
|
||||
public long getJoined() {
|
||||
return this.joined;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package org.qora.data.transaction;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qora.transaction.Transaction.TransactionType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@Schema(allOf = { TransactionData.class })
|
||||
public class JoinGroupTransactionData extends TransactionData {
|
||||
|
||||
// Properties
|
||||
@Schema(description = "joiner's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||
private byte[] joinerPublicKey;
|
||||
@Schema(description = "which group to update", example = "my-group")
|
||||
private String groupName;
|
||||
|
||||
// Constructors
|
||||
|
||||
// For JAX-RS
|
||||
protected JoinGroupTransactionData() {
|
||||
super(TransactionType.JOIN_GROUP);
|
||||
}
|
||||
|
||||
public void afterUnmarshal(Unmarshaller u, Object parent) {
|
||||
this.creatorPublicKey = this.joinerPublicKey;
|
||||
}
|
||||
|
||||
public JoinGroupTransactionData(byte[] joinerPublicKey, String groupName, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
|
||||
super(TransactionType.JOIN_GROUP, fee, joinerPublicKey, timestamp, reference, signature);
|
||||
|
||||
this.joinerPublicKey = joinerPublicKey;
|
||||
this.groupName = groupName;
|
||||
}
|
||||
|
||||
public JoinGroupTransactionData(byte[] joinerPublicKey, String groupName, BigDecimal fee, long timestamp, byte[] reference) {
|
||||
this(joinerPublicKey, groupName, fee, timestamp, reference, null);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
|
||||
public byte[] getJoinerPublicKey() {
|
||||
return this.joinerPublicKey;
|
||||
}
|
||||
|
||||
public String getGroupName() {
|
||||
return this.groupName;
|
||||
}
|
||||
|
||||
}
|
@ -33,7 +33,9 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
|
||||
IssueAssetTransactionData.class, TransferAssetTransactionData.class,
|
||||
CreateOrderTransactionData.class, CancelOrderTransactionData.class,
|
||||
MultiPaymentTransactionData.class, DeployATTransactionData.class, MessageTransactionData.class, ATTransactionData.class,
|
||||
CreateGroupTransactionData.class, UpdateGroupTransactionData.class})
|
||||
CreateGroupTransactionData.class, UpdateGroupTransactionData.class,
|
||||
JoinGroupTransactionData.class
|
||||
})
|
||||
//All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public abstract class TransactionData {
|
||||
|
@ -1,7 +1,12 @@
|
||||
package org.qora.group;
|
||||
|
||||
import org.qora.account.Account;
|
||||
import org.qora.account.PublicKeyAccount;
|
||||
import org.qora.data.group.GroupAdminData;
|
||||
import org.qora.data.group.GroupData;
|
||||
import org.qora.data.group.GroupMemberData;
|
||||
import org.qora.data.transaction.CreateGroupTransactionData;
|
||||
import org.qora.data.transaction.JoinGroupTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.data.transaction.UpdateGroupTransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
@ -48,9 +53,16 @@ public class Group {
|
||||
|
||||
public void create() throws DataException {
|
||||
this.repository.getGroupRepository().save(this.groupData);
|
||||
|
||||
// Add owner as admin too
|
||||
this.repository.getGroupRepository().save(new GroupAdminData(this.groupData.getGroupName(), this.groupData.getOwner()));
|
||||
|
||||
// Add owner as member too
|
||||
this.repository.getGroupRepository().save(new GroupMemberData(this.groupData.getGroupName(), this.groupData.getOwner(), this.groupData.getCreated()));
|
||||
}
|
||||
|
||||
public void uncreate() throws DataException {
|
||||
// Repository takes care of cleaning up ancilliary data!
|
||||
this.repository.getGroupRepository().delete(this.groupData.getGroupName());
|
||||
}
|
||||
|
||||
@ -59,6 +71,8 @@ public class Group {
|
||||
if (previousTransactionData == null)
|
||||
throw new DataException("Unable to revert group transaction as referenced transaction not found in repository");
|
||||
|
||||
// XXX needs code to reinstate owner as admin and member
|
||||
|
||||
switch (previousTransactionData.getType()) {
|
||||
case CREATE_GROUP:
|
||||
CreateGroupTransactionData previousCreateGroupTransactionData = (CreateGroupTransactionData) previousTransactionData;
|
||||
@ -96,6 +110,11 @@ public class Group {
|
||||
|
||||
// Save updated group data
|
||||
this.repository.getGroupRepository().save(this.groupData);
|
||||
|
||||
// XXX new owner should be an admin if not already
|
||||
// XXX new owner should be a member if not already
|
||||
|
||||
// XXX what happens to previous owner? retained as admin?
|
||||
}
|
||||
|
||||
public void revert(UpdateGroupTransactionData updateGroupTransactionData) throws DataException {
|
||||
@ -109,4 +128,18 @@ public class Group {
|
||||
this.repository.getGroupRepository().save(this.groupData);
|
||||
}
|
||||
|
||||
public void join(JoinGroupTransactionData joinGroupTransactionData) throws DataException {
|
||||
Account joiner = new PublicKeyAccount(this.repository, joinGroupTransactionData.getJoinerPublicKey());
|
||||
|
||||
GroupMemberData groupMemberData = new GroupMemberData(joinGroupTransactionData.getGroupName(), joiner.getAddress(), joinGroupTransactionData.getTimestamp());
|
||||
this.repository.getGroupRepository().save(groupMemberData);
|
||||
}
|
||||
|
||||
public void unjoin(JoinGroupTransactionData joinGroupTransactionData) throws DataException {
|
||||
Account joiner = new PublicKeyAccount(this.repository, joinGroupTransactionData.getJoinerPublicKey());
|
||||
|
||||
GroupMemberData groupMemberData = new GroupMemberData(joinGroupTransactionData.getGroupName(), joiner.getAddress(), joinGroupTransactionData.getTimestamp());
|
||||
this.repository.getGroupRepository().delete(groupMemberData);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,10 +2,14 @@ package org.qora.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.qora.data.group.GroupAdminData;
|
||||
import org.qora.data.group.GroupData;
|
||||
import org.qora.data.group.GroupMemberData;
|
||||
|
||||
public interface GroupRepository {
|
||||
|
||||
// Groups
|
||||
|
||||
public GroupData fromGroupName(String groupName) throws DataException;
|
||||
|
||||
public boolean groupExists(String groupName) throws DataException;
|
||||
@ -18,4 +22,25 @@ public interface GroupRepository {
|
||||
|
||||
public void delete(String groupName) throws DataException;
|
||||
|
||||
}
|
||||
// Group Admins
|
||||
|
||||
public List<GroupAdminData> getAllGroupAdmins(String groupName) throws DataException;
|
||||
|
||||
public void save(GroupAdminData groupAdminData) throws DataException;
|
||||
|
||||
public void delete(GroupAdminData groupAdminData) throws DataException;
|
||||
|
||||
// Group Members
|
||||
|
||||
public boolean memberExists(String groupName, String member) throws DataException;
|
||||
|
||||
public List<GroupMemberData> getAllGroupMembers(String groupName) throws DataException;
|
||||
|
||||
/** Returns number of group members, or null if group doesn't exist */
|
||||
public Integer countGroupMembers(String groupName) throws DataException;
|
||||
|
||||
public void save(GroupMemberData groupMemberData) throws DataException;
|
||||
|
||||
public void delete(GroupMemberData groupMemberData) throws DataException;
|
||||
|
||||
}
|
@ -414,10 +414,12 @@ public class HSQLDBDatabaseUpdates {
|
||||
+ "reference Signature, PRIMARY KEY (group_name))");
|
||||
// For finding groups by owner
|
||||
stmt.execute("CREATE INDEX AccountGroupOwnerIndex on AccountGroups (owner)");
|
||||
|
||||
// Admins
|
||||
stmt.execute("CREATE TABLE AccountGroupAdmins (group_name GroupName, admin QoraAddress, PRIMARY KEY (group_name, admin))");
|
||||
// For finding groups that address administrates
|
||||
stmt.execute("CREATE INDEX AccountGroupAdminIndex on AccountGroupAdmins (admin)");
|
||||
|
||||
// Members
|
||||
stmt.execute("CREATE TABLE AccountGroupMembers (group_name GroupName, address QoraAddress, joined TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (group_name, address))");
|
||||
// For finding groups that address is member
|
||||
@ -450,6 +452,14 @@ public class HSQLDBDatabaseUpdates {
|
||||
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
|
||||
break;
|
||||
|
||||
case 32:
|
||||
// Account group join/leave transactions
|
||||
stmt.execute("CREATE TABLE JoinGroupTransactions (signature Signature, joiner QoraPublicKey NOT NULL, group_name GroupName NOT NULL, "
|
||||
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
|
||||
stmt.execute("CREATE TABLE LeaveGroupTransactions (signature Signature, leaver QoraPublicKey NOT NULL, group_name GroupName NOT NULL, "
|
||||
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@ -7,7 +7,9 @@ import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import org.qora.data.group.GroupAdminData;
|
||||
import org.qora.data.group.GroupData;
|
||||
import org.qora.data.group.GroupMemberData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.GroupRepository;
|
||||
|
||||
@ -19,6 +21,8 @@ public class HSQLDBGroupRepository implements GroupRepository {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
// Groups
|
||||
|
||||
@Override
|
||||
public GroupData fromGroupName(String groupName) throws DataException {
|
||||
try (ResultSet resultSet = this.repository
|
||||
@ -135,10 +139,129 @@ public class HSQLDBGroupRepository implements GroupRepository {
|
||||
@Override
|
||||
public void delete(String groupName) throws DataException {
|
||||
try {
|
||||
// Remove invites
|
||||
this.repository.delete("AccountGroupInvites", "group_name = ?", groupName);
|
||||
// Remove bans
|
||||
this.repository.delete("AccountGroupBans", "group_name = ?", groupName);
|
||||
// Remove members
|
||||
this.repository.delete("AccountGroupMembers", "group_name = ?", groupName);
|
||||
// Remove admins
|
||||
this.repository.delete("AccountGroupAdmins", "group_name = ?", groupName);
|
||||
// Remove group
|
||||
this.repository.delete("AccountGroups", "group_name = ?", groupName);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete group info from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Group Admins
|
||||
|
||||
@Override
|
||||
public List<GroupAdminData> getAllGroupAdmins(String groupName) throws DataException {
|
||||
List<GroupAdminData> admins = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin FROM AccountGroupAdmins WHERE group_name = ?", groupName)) {
|
||||
if (resultSet == null)
|
||||
return admins;
|
||||
|
||||
do {
|
||||
String admin = resultSet.getString(1);
|
||||
|
||||
admins.add(new GroupAdminData(groupName, admin));
|
||||
} while (resultSet.next());
|
||||
|
||||
return admins;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch group admins from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(GroupAdminData groupAdminData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountGroupAdmins");
|
||||
|
||||
saveHelper.bind("group_name", groupAdminData.getGroupName()).bind("admin", groupAdminData.getAdmin());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save group admin info into repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(GroupAdminData groupAdminData) throws DataException {
|
||||
try {
|
||||
this.repository.delete("AccountGroupAdmins", "group_name = ? AND admin = ?", groupAdminData.getGroupName(), groupAdminData.getAdmin());
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete group admin info from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Group Members
|
||||
|
||||
@Override
|
||||
public boolean memberExists(String groupName, String member) throws DataException {
|
||||
try {
|
||||
return this.repository.exists("AccountGroupMembers", "group_name = ? AND address = ?", groupName, member);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to check for group member in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupMemberData> getAllGroupMembers(String groupName) throws DataException {
|
||||
List<GroupMemberData> members = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute("SELECT address, joined FROM AccountGroupMembers WHERE group_name = ?", groupName)) {
|
||||
if (resultSet == null)
|
||||
return members;
|
||||
|
||||
do {
|
||||
String member = resultSet.getString(1);
|
||||
long joined = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
|
||||
|
||||
members.add(new GroupMemberData(groupName, member, joined));
|
||||
} while (resultSet.next());
|
||||
|
||||
return members;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch group members from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countGroupMembers(String groupName) throws DataException {
|
||||
try (ResultSet resultSet = this.repository.checkedExecute("SELECT group_name, COUNT(*) FROM AccountGroupMembers WHERE group_name = ? GROUP BY group_name", groupName)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
return resultSet.getInt(2);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch group member count from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(GroupMemberData groupMemberData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountGroupMembers");
|
||||
|
||||
saveHelper.bind("group_name", groupMemberData.getGroupName()).bind("address", groupMemberData.getMember()).bind("joined", new Timestamp(groupMemberData.getJoined()));
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save group member info into repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(GroupMemberData groupMemberData) throws DataException {
|
||||
try {
|
||||
this.repository.delete("AccountGroupMembers", "group_name = ? AND address = ?", groupMemberData.getGroupName(), groupMemberData.getMember());
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete group member info from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
package org.qora.repository.hsqldb.transaction;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.qora.data.transaction.JoinGroupTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.hsqldb.HSQLDBRepository;
|
||||
import org.qora.repository.hsqldb.HSQLDBSaver;
|
||||
|
||||
public class HSQLDBJoinGroupTransactionRepository extends HSQLDBTransactionRepository {
|
||||
|
||||
public HSQLDBJoinGroupTransactionRepository(HSQLDBRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
|
||||
try (ResultSet resultSet = this.repository.checkedExecute("SELECT group_name FROM JoinGroupTransactions WHERE signature = ?", signature)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
String groupName = resultSet.getString(1);
|
||||
|
||||
return new JoinGroupTransactionData(creatorPublicKey, groupName, fee, timestamp, reference, signature);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch join group transaction from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(TransactionData transactionData) throws DataException {
|
||||
JoinGroupTransactionData joinGroupTransactionData = (JoinGroupTransactionData) transactionData;
|
||||
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("JoinGroupTransactions");
|
||||
|
||||
saveHelper.bind("signature", joinGroupTransactionData.getSignature()).bind("joiner", joinGroupTransactionData.getJoinerPublicKey())
|
||||
.bind("group_name", joinGroupTransactionData.getGroupName());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save join group transaction into repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -43,6 +43,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
private HSQLDBATTransactionRepository atTransactionRepository;
|
||||
private HSQLDBCreateGroupTransactionRepository createGroupTransactionRepository;
|
||||
private HSQLDBUpdateGroupTransactionRepository updateGroupTransactionRepository;
|
||||
private HSQLDBJoinGroupTransactionRepository joinGroupTransactionRepository;
|
||||
|
||||
public HSQLDBTransactionRepository(HSQLDBRepository repository) {
|
||||
this.repository = repository;
|
||||
@ -66,6 +67,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
this.atTransactionRepository = new HSQLDBATTransactionRepository(repository);
|
||||
this.createGroupTransactionRepository = new HSQLDBCreateGroupTransactionRepository(repository);
|
||||
this.updateGroupTransactionRepository = new HSQLDBUpdateGroupTransactionRepository(repository);
|
||||
this.joinGroupTransactionRepository = new HSQLDBJoinGroupTransactionRepository(repository);
|
||||
}
|
||||
|
||||
protected HSQLDBTransactionRepository() {
|
||||
@ -198,6 +200,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
case UPDATE_GROUP:
|
||||
return this.updateGroupTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
|
||||
|
||||
case JOIN_GROUP:
|
||||
return this.joinGroupTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
|
||||
|
||||
default:
|
||||
throw new DataException("Unsupported transaction type [" + type.name() + "] during fetch from HSQLDB repository");
|
||||
}
|
||||
@ -526,6 +531,10 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
this.updateGroupTransactionRepository.save(transactionData);
|
||||
break;
|
||||
|
||||
case JOIN_GROUP:
|
||||
this.joinGroupTransactionRepository.save(transactionData);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new DataException("Unsupported transaction type [" + transactionData.getType().name() + "] during save into HSQLDB repository");
|
||||
}
|
||||
|
139
src/main/java/org/qora/transaction/JoinGroupTransaction.java
Normal file
139
src/main/java/org/qora/transaction/JoinGroupTransaction.java
Normal file
@ -0,0 +1,139 @@
|
||||
package org.qora.transaction;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.qora.account.Account;
|
||||
import org.qora.account.PublicKeyAccount;
|
||||
import org.qora.asset.Asset;
|
||||
import org.qora.data.transaction.JoinGroupTransactionData;
|
||||
import org.qora.data.group.GroupData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.group.Group;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
|
||||
import com.google.common.base.Utf8;
|
||||
|
||||
public class JoinGroupTransaction extends Transaction {
|
||||
|
||||
// Properties
|
||||
private JoinGroupTransactionData joinGroupTransactionData;
|
||||
|
||||
// Constructors
|
||||
|
||||
public JoinGroupTransaction(Repository repository, TransactionData transactionData) {
|
||||
super(repository, transactionData);
|
||||
|
||||
this.joinGroupTransactionData = (JoinGroupTransactionData) this.transactionData;
|
||||
}
|
||||
|
||||
// More information
|
||||
|
||||
@Override
|
||||
public List<Account> getRecipientAccounts() throws DataException {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInvolved(Account account) throws DataException {
|
||||
String address = account.getAddress();
|
||||
|
||||
if (address.equals(this.getJoiner().getAddress()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal getAmount(Account account) throws DataException {
|
||||
String address = account.getAddress();
|
||||
BigDecimal amount = BigDecimal.ZERO.setScale(8);
|
||||
|
||||
if (address.equals(this.getJoiner().getAddress()))
|
||||
amount = amount.subtract(this.transactionData.getFee());
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
// Navigation
|
||||
|
||||
public Account getJoiner() throws DataException {
|
||||
return new PublicKeyAccount(this.repository, this.joinGroupTransactionData.getJoinerPublicKey());
|
||||
}
|
||||
|
||||
// Processing
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Check group name size bounds
|
||||
int groupNameLength = Utf8.encodedLength(joinGroupTransactionData.getGroupName());
|
||||
if (groupNameLength < 1 || groupNameLength > Group.MAX_NAME_SIZE)
|
||||
return ValidationResult.INVALID_NAME_LENGTH;
|
||||
|
||||
// Check group name is lowercase
|
||||
if (!joinGroupTransactionData.getGroupName().equals(joinGroupTransactionData.getGroupName().toLowerCase()))
|
||||
return ValidationResult.NAME_NOT_LOWER_CASE;
|
||||
|
||||
GroupData groupData = this.repository.getGroupRepository().fromGroupName(joinGroupTransactionData.getGroupName());
|
||||
|
||||
// Check group exists
|
||||
if (groupData == null)
|
||||
return ValidationResult.GROUP_DOES_NOT_EXIST;
|
||||
|
||||
Account joiner = getJoiner();
|
||||
|
||||
if (this.repository.getGroupRepository().memberExists(joinGroupTransactionData.getGroupName(), joiner.getAddress()))
|
||||
return ValidationResult.ALREADY_GROUP_MEMBER;
|
||||
|
||||
// Check fee is positive
|
||||
if (joinGroupTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0)
|
||||
return ValidationResult.NEGATIVE_FEE;
|
||||
|
||||
if (!Arrays.equals(joiner.getLastReference(), joinGroupTransactionData.getReference()))
|
||||
return ValidationResult.INVALID_REFERENCE;
|
||||
|
||||
// Check creator has enough funds
|
||||
if (joiner.getConfirmedBalance(Asset.QORA).compareTo(joinGroupTransactionData.getFee()) < 0)
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Group Membership
|
||||
Group group = new Group(this.repository, joinGroupTransactionData.getGroupName());
|
||||
group.join(joinGroupTransactionData);
|
||||
|
||||
// Save this transaction
|
||||
this.repository.getTransactionRepository().save(joinGroupTransactionData);
|
||||
|
||||
// Update joiner's balance
|
||||
Account joiner = getJoiner();
|
||||
joiner.setConfirmedBalance(Asset.QORA, joiner.getConfirmedBalance(Asset.QORA).subtract(joinGroupTransactionData.getFee()));
|
||||
|
||||
// Update joiner's reference
|
||||
joiner.setLastReference(joinGroupTransactionData.getSignature());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void orphan() throws DataException {
|
||||
// Revert group membership
|
||||
Group group = new Group(this.repository, joinGroupTransactionData.getGroupName());
|
||||
group.unjoin(joinGroupTransactionData);
|
||||
|
||||
// Delete this transaction itself
|
||||
this.repository.getTransactionRepository().delete(joinGroupTransactionData);
|
||||
|
||||
// Update joiner's balance
|
||||
Account joiner = getJoiner();
|
||||
joiner.setConfirmedBalance(Asset.QORA, joiner.getConfirmedBalance(Asset.QORA).add(joinGroupTransactionData.getFee()));
|
||||
|
||||
// Update joiner's reference
|
||||
joiner.setLastReference(joinGroupTransactionData.getReference());
|
||||
}
|
||||
|
||||
}
|
@ -128,6 +128,7 @@ public abstract class Transaction {
|
||||
GROUP_ALREADY_EXISTS(48),
|
||||
GROUP_DOES_NOT_EXIST(49),
|
||||
INVALID_GROUP_OWNER(50),
|
||||
ALREADY_GROUP_MEMBER(51),
|
||||
NOT_YET_RELEASED(1000);
|
||||
|
||||
public final int value;
|
||||
@ -233,6 +234,9 @@ public abstract class Transaction {
|
||||
case UPDATE_GROUP:
|
||||
return new UpdateGroupTransaction(repository, transactionData);
|
||||
|
||||
case JOIN_GROUP:
|
||||
return new JoinGroupTransaction(repository, transactionData);
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported transaction type [" + transactionData.getType().value + "] during fetch from repository");
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ public class UpdateGroupTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public void orphan() throws DataException {
|
||||
// Revert name
|
||||
// Revert Group update
|
||||
Group group = new Group(this.repository, updateGroupTransactionData.getGroupName());
|
||||
group.revert(updateGroupTransactionData);
|
||||
|
||||
|
@ -0,0 +1,99 @@
|
||||
package org.qora.transform.transaction;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.qora.account.PublicKeyAccount;
|
||||
import org.qora.data.transaction.JoinGroupTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.group.Group;
|
||||
import org.qora.transform.TransformationException;
|
||||
import org.qora.utils.Serialization;
|
||||
|
||||
import com.google.common.base.Utf8;
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
public class JoinGroupTransactionTransformer extends TransactionTransformer {
|
||||
|
||||
// Property lengths
|
||||
private static final int JOINER_LENGTH = PUBLIC_KEY_LENGTH;
|
||||
private static final int NAME_SIZE_LENGTH = INT_LENGTH;
|
||||
|
||||
private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + JOINER_LENGTH + NAME_SIZE_LENGTH;
|
||||
|
||||
static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
|
||||
long timestamp = byteBuffer.getLong();
|
||||
|
||||
byte[] reference = new byte[REFERENCE_LENGTH];
|
||||
byteBuffer.get(reference);
|
||||
|
||||
byte[] joinerPublicKey = Serialization.deserializePublicKey(byteBuffer);
|
||||
|
||||
String groupName = Serialization.deserializeSizedString(byteBuffer, Group.MAX_NAME_SIZE);
|
||||
|
||||
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
|
||||
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
byteBuffer.get(signature);
|
||||
|
||||
return new JoinGroupTransactionData(joinerPublicKey, groupName, fee, timestamp, reference, signature);
|
||||
}
|
||||
|
||||
public static int getDataLength(TransactionData transactionData) throws TransformationException {
|
||||
JoinGroupTransactionData joinGroupTransactionData = (JoinGroupTransactionData) transactionData;
|
||||
|
||||
int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(joinGroupTransactionData.getGroupName());
|
||||
|
||||
return dataLength;
|
||||
}
|
||||
|
||||
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
|
||||
try {
|
||||
JoinGroupTransactionData joinGroupTransactionData = (JoinGroupTransactionData) transactionData;
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(Ints.toByteArray(joinGroupTransactionData.getType().value));
|
||||
bytes.write(Longs.toByteArray(joinGroupTransactionData.getTimestamp()));
|
||||
bytes.write(joinGroupTransactionData.getReference());
|
||||
|
||||
bytes.write(joinGroupTransactionData.getCreatorPublicKey());
|
||||
Serialization.serializeSizedString(bytes, joinGroupTransactionData.getGroupName());
|
||||
|
||||
Serialization.serializeBigDecimal(bytes, joinGroupTransactionData.getFee());
|
||||
|
||||
if (joinGroupTransactionData.getSignature() != null)
|
||||
bytes.write(joinGroupTransactionData.getSignature());
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException | ClassCastException e) {
|
||||
throw new TransformationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static JSONObject toJSON(TransactionData transactionData) throws TransformationException {
|
||||
JSONObject json = TransactionTransformer.getBaseJSON(transactionData);
|
||||
|
||||
try {
|
||||
JoinGroupTransactionData joinGroupTransactionData = (JoinGroupTransactionData) transactionData;
|
||||
|
||||
byte[] joinerPublicKey = joinGroupTransactionData.getJoinerPublicKey();
|
||||
|
||||
json.put("joiner", PublicKeyAccount.getAddress(joinerPublicKey));
|
||||
json.put("joinerPublicKey", HashCode.fromBytes(joinerPublicKey).toString());
|
||||
|
||||
json.put("groupName", joinGroupTransactionData.getGroupName());
|
||||
} catch (ClassCastException e) {
|
||||
throw new TransformationException(e);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
}
|
@ -100,6 +100,9 @@ public class TransactionTransformer extends Transformer {
|
||||
case UPDATE_GROUP:
|
||||
return UpdateGroupTransactionTransformer.fromByteBuffer(byteBuffer);
|
||||
|
||||
case JOIN_GROUP:
|
||||
return JoinGroupTransactionTransformer.fromByteBuffer(byteBuffer);
|
||||
|
||||
default:
|
||||
throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion from bytes");
|
||||
}
|
||||
@ -167,6 +170,9 @@ public class TransactionTransformer extends Transformer {
|
||||
case UPDATE_GROUP:
|
||||
return UpdateGroupTransactionTransformer.getDataLength(transactionData);
|
||||
|
||||
case JOIN_GROUP:
|
||||
return JoinGroupTransactionTransformer.getDataLength(transactionData);
|
||||
|
||||
default:
|
||||
throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] when requesting byte length");
|
||||
}
|
||||
@ -231,6 +237,9 @@ public class TransactionTransformer extends Transformer {
|
||||
case UPDATE_GROUP:
|
||||
return UpdateGroupTransactionTransformer.toBytes(transactionData);
|
||||
|
||||
case JOIN_GROUP:
|
||||
return JoinGroupTransactionTransformer.toBytes(transactionData);
|
||||
|
||||
default:
|
||||
throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] during conversion to bytes");
|
||||
}
|
||||
@ -304,6 +313,9 @@ public class TransactionTransformer extends Transformer {
|
||||
case UPDATE_GROUP:
|
||||
return UpdateGroupTransactionTransformer.toBytesForSigningImpl(transactionData);
|
||||
|
||||
case JOIN_GROUP:
|
||||
return JoinGroupTransactionTransformer.toBytesForSigningImpl(transactionData);
|
||||
|
||||
default:
|
||||
throw new TransformationException(
|
||||
"Unsupported transaction type [" + transactionData.getType().value + "] during conversion to bytes for signing");
|
||||
@ -389,6 +401,9 @@ public class TransactionTransformer extends Transformer {
|
||||
case UPDATE_GROUP:
|
||||
return UpdateGroupTransactionTransformer.toJSON(transactionData);
|
||||
|
||||
case JOIN_GROUP:
|
||||
return JoinGroupTransactionTransformer.toJSON(transactionData);
|
||||
|
||||
default:
|
||||
throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] during conversion to JSON");
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user