package org.karmaexchange.dao; import static java.lang.String.format; import static org.karmaexchange.util.OfyService.ofy; import javax.annotation.Nullable; import javax.xml.bind.annotation.XmlTransient; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import org.karmaexchange.resources.msg.AuthorizationErrorInfo; import org.karmaexchange.resources.msg.ErrorResponseMsg; import org.karmaexchange.resources.msg.ErrorResponseMsg.ErrorInfo; import org.karmaexchange.resources.msg.ValidationErrorInfo.ValidationError; import org.karmaexchange.resources.msg.ValidationErrorInfo.ValidationErrorType; import org.karmaexchange.util.OfyUtil; import org.karmaexchange.util.UserService; import com.googlecode.objectify.Key; import com.googlecode.objectify.VoidWork; import com.googlecode.objectify.annotation.Ignore; import com.googlecode.objectify.annotation.OnLoad; import com.googlecode.objectify.annotation.Parent; @Data public abstract class BaseDao<T extends BaseDao<T>> { @Parent protected Key<?> owner; @Ignore private String key; private ModificationInfo modificationInfo; @Ignore protected Permission permission; public final void setOwner(String keyStr) { owner = (keyStr == null) ? null : Key.<Object>create(keyStr); } public final String getOwner() { return (owner == null) ? null : owner.getString(); } public static <T extends BaseDao<T>> void upsert(T resource) { ofy().transact(new UpsertTxn<T>(resource)); } @Data @EqualsAndHashCode(callSuper=false) public static class UpsertTxn<T extends BaseDao<T>> extends VoidWork { private final T resource; public void vrun() { // Cleanup any id and key mismatch. resource.syncKeyAndIdForUpsert(); T prevResource = null; if (resource.isKeyComplete()) { prevResource = ofy().load().key(Key.create(resource)).now(); } if (prevResource == null) { resource.insert(); } else { resource.update(prevResource); } } } protected abstract void syncKeyAndIdForUpsert(); @XmlTransient public abstract boolean isKeyComplete(); public static <T extends BaseDao<T>> void partialUpdate(T resource) { resource.partialUpdate(); } public static <T extends BaseDao<T>> void delete(Key<T> key) { ofy().transact(new DeleteTxn<T>(key)); } @Data @EqualsAndHashCode(callSuper=false) public static class DeleteTxn<T extends BaseDao<T>> extends VoidWork { private final Key<T> resourceKey; public void vrun() { T resource = ofy().load().key(resourceKey).now(); if (resource != null) { resource.delete(); } } } final void insert() { preProcessInsert(); validateMutationPermission(); ofy().save().entity(this).now(); postProcessInsert(); } final void update(T prevObj) { processUpdate(prevObj); validateMutationPermission(); ofy().save().entity(this).now(); } final void partialUpdate() { // Partial updates have task-specific permission rules vs. per object type permission rules. // But all mutations require either a logged in user or an admin user. validateLoginStatusForMutation(); processPartialUpdate(); ofy().save().entity(this).now(); } final void delete() { processDelete(); validateMutationPermission(); ofy().delete().key(Key.create(this)).now(); } private void validateMutationPermission() { validateLoginStatusForMutation(); updatePermission(); if (!permission.canEdit()) { throw AuthorizationErrorInfo.createException(this); } } private void validateLoginStatusForMutation() { if (UserService.isNotLoggedInUser()) { throw ErrorResponseMsg.createException("Login required", ErrorInfo.Type.LOGIN_REQUIRED); } } protected void preProcessInsert() { setModificationInfo(ModificationInfo.create()); } protected void postProcessInsert() { updateKey(); } protected void processUpdate(T prevObj) { updateKey(); if (!prevObj.getKey().equals(getKey())) { throw ErrorResponseMsg.createException( format("thew new resource key [%s] does not match the previous key [%s]", getKey(), prevObj.getKey()), ErrorInfo.Type.BAD_REQUEST); } updateModificationInfo(prevObj); } protected void processPartialUpdate() { updateModificationInfo(null); } private final void updateModificationInfo(@Nullable T prevObj) { if (getModificationInfo() == null) { // Handle objects that were created without modification info. if ((prevObj == null) || (prevObj.getModificationInfo() == null)) { setModificationInfo(ModificationInfo.create()); } else { setModificationInfo(prevObj.getModificationInfo()); } } getModificationInfo().update(); } protected void processDelete() { } @OnLoad public void processLoad() { updateKey(); updatePermission(); } protected void updateKey() { setKey(KeyWrapper.create(this).getKey()); } protected final void updatePermission() { if (UserService.isCurrentUserAdmin()) { permission = Permission.ALL; } else if (!UserService.isCurrentUserLoggedIn()) { permission = Permission.READ; } else { permission = evalPermission(); } } protected abstract Permission evalPermission(); public void updateDependentNamedKeys() { // Do nothing by default. } @Data @NoArgsConstructor @EqualsAndHashCode(callSuper=true) @ToString(callSuper=true) public static class ResourceValidationError extends ValidationError { @Nullable private String resourceKind; @Nullable private String resourceKey; @Nullable private String field; public ResourceValidationError(BaseDao<?> resource, ValidationErrorType errorType, @Nullable String fieldName) { super(errorType); if (resource.isKeyComplete()) { resourceKey = Key.create(resource).getString(); } resourceKind = OfyUtil.getKind(resource.getClass()); this.field = fieldName; } } @Data @NoArgsConstructor @EqualsAndHashCode(callSuper=true) @ToString(callSuper=true) public static class MultiFieldResourceValidationError extends ResourceValidationError { private String otherField; public MultiFieldResourceValidationError(BaseDao<?> resource, ValidationErrorType errorType, String fieldName, String otherFieldName) { super(resource, errorType, fieldName); this.otherField = otherFieldName; } } @Data @NoArgsConstructor @EqualsAndHashCode(callSuper=true) @ToString(callSuper=true) public static class LimitResourceValidationError extends ResourceValidationError { private int limit; public LimitResourceValidationError(BaseDao<?> resource, ValidationErrorType errorType, String fieldName, int limit) { super(resource, errorType, fieldName); this.limit = limit; } } @Data @NoArgsConstructor @EqualsAndHashCode(callSuper=true) @ToString(callSuper=true) public static class ListValueValidationError extends ResourceValidationError { private String memberValue; public ListValueValidationError(BaseDao<?> resource, ValidationErrorType errorType, String fieldName, String memberValue) { super(resource, errorType, fieldName); this.memberValue = memberValue; } } }