Skip to content

Commit

Permalink
Add base argument class for conditionally reading objects.
Browse files Browse the repository at this point in the history
`GetObject`, `HeadObject` and `CopyObject` S3 API support conditions
on reading objects. This patch introduces common base class
`ObjectConditionalReadArgs` for `GetObjectArgs`, `StatObjectArgs`,
`CopySource` and `ComposeSource` to avoid code duplication.
  • Loading branch information
balamurugana committed Jun 24, 2020
1 parent 0014bf3 commit 5f4a9dd
Show file tree
Hide file tree
Showing 12 changed files with 638 additions and 644 deletions.
33 changes: 22 additions & 11 deletions api/src/main/java/io/minio/BaseArgs.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -119,24 +120,34 @@ public B extraQueryParams(Map<String, String> queryParams) {
return (B) this;
}

/** Creates derived Args class with each attribute populated. */
@SuppressWarnings("unchecked") // safe as B will always be the builder of the current args class
public A build() throws IllegalArgumentException {
private A newInstance() {
try {
A args = (A) this.getClass().getEnclosingClass().getDeclaredConstructor().newInstance();
operations.forEach(operation -> operation.accept(args));
validate(args);
return args;
for (Constructor<?> constructor :
this.getClass().getEnclosingClass().getDeclaredConstructors()) {
if (constructor.getParameterCount() == 0) {
return (A) constructor.newInstance();
}
}

throw new RuntimeException(
this.getClass().getEnclosingClass() + " must have no argument constructor");
} catch (InstantiationException
| IllegalAccessException
| InvocationTargetException
| NoSuchMethodException
| SecurityException e) {
// This should never happen as we'll always have the
// Builder class as an enclosed class of the args class
e.printStackTrace();
return null;
// Args class must have no argument constructor with at least protected access.
throw new RuntimeException(e);
}
}

/** Creates derived Args class with each attribute populated. */
@SuppressWarnings("unchecked") // safe as B will always be the builder of the current args class
public A build() throws IllegalArgumentException {
A args = newInstance();
operations.forEach(operation -> operation.accept(args));
validate(args);
return args;
}
}
}
41 changes: 35 additions & 6 deletions api/src/main/java/io/minio/ComposeObjectArgs.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,32 @@

package io.minio;

import java.util.LinkedList;
import java.util.List;
import okhttp3.HttpUrl;

public class ComposeObjectArgs extends ObjectWriteArgs {
List<ComposeSource> sources;

protected ComposeObjectArgs() {
}

public ComposeObjectArgs(CopyObjectArgs args) {
this.extraHeaders = args.extraHeaders;
this.extraQueryParams = args.extraQueryParams;
this.bucketName = args.bucketName;
this.region = args.region;
this.objectName = args.objectName;
this.headers = args.headers;
this.userMetadata = args.userMetadata;
this.sse = args.sse;
this.tags = args.tags;
this.retention = args.retention;
this.legalHold = args.legalHold;
this.sources = new LinkedList<>();
this.sources.add(new ComposeSource(args.source()));
}

public List<ComposeSource> sources() {
return sources;
}
Expand All @@ -29,7 +50,21 @@ public static Builder builder() {
return new Builder();
}

@Override
public void validateSse(HttpUrl url) {
super.validateSse(url);
for (ComposeSource source : sources) {
source.validateSsec(url);
}
}

public static final class Builder extends ObjectWriteArgs.Builder<Builder, ComposeObjectArgs> {
private void validateSources(List<ComposeSource> sources) {
if (sources == null || sources.isEmpty()) {
throw new IllegalArgumentException("compose sources cannot be empty");
}
}

@Override
protected void validate(ComposeObjectArgs args) {
super.validate(args);
Expand All @@ -41,11 +76,5 @@ public Builder sources(List<ComposeSource> sources) {
operations.add(args -> args.sources = sources);
return this;
}

private void validateSources(List<ComposeSource> sources) {
if (sources == null || sources.isEmpty()) {
throw new IllegalArgumentException("compose sources cannot be empty");
}
}
}
}
216 changes: 78 additions & 138 deletions api/src/main/java/io/minio/ComposeSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,84 +20,78 @@
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import java.time.ZonedDateTime;
import okhttp3.HttpUrl;

/** Source information to compose object. */
public class ComposeSource extends ObjectVersionArgs {
private Long offset;
private Long length;
private long objectSize;
private String matchETag;
private String notMatchETag;
private ZonedDateTime modifiedSince;
private ZonedDateTime unmodifiedSince;
private ServerSideEncryptionCustomerKey ssec;

private Multimap<String, String> headers;

public Long offset() {
return offset;
import io.minio.errors.InternalException;

/** A source object defintion for {@link ComposeObjectArgs}. */
public class ComposeSource extends ObjectConditionalReadArgs {
private Long objectSize = null;
private Multimap<String, String> headers = null;

protected ComposeSource() {}

public ComposeSource(ObjectConditionalReadArgs args) {
this.extraHeaders = args.extraHeaders;
this.extraQueryParams = args.extraQueryParams;
this.bucketName = args.bucketName;
this.region = args.region;
this.objectName = args.objectName;
this.versionId = args.versionId;
this.ssec = args.ssec;
this.offset = args.offset;
this.length = args.length;
this.matchETag = args.matchETag;
this.notMatchETag = args.notMatchETag;
this.modifiedSince = args.modifiedSince;
this.unmodifiedSince = args.unmodifiedSince;
}

public Long length() {
return length;
}

public long objectSize() {
return objectSize;
}

public String matchETag() {
return matchETag;
}

public String notMatchETag() {
return notMatchETag;
}
private void throwException(long objectsize, long arg, String argName) {
StringBuilder builder =
new StringBuilder().append("source ").append(bucketName).append("/").append(objectName);

public ZonedDateTime modifiedSince() {
return modifiedSince;
}
if (versionId != null) {
builder.append("?versionId=").append(versionId);
}

public ZonedDateTime unmodifiedSince() {
return unmodifiedSince;
}
builder
.append(": ")
.append(argName)
.append(" ")
.append(arg)
.append(" is beyond object size ")
.append(objectSize);

public Multimap<String, String> headers() {
return headers;
throw new IllegalArgumentException(builder.toString());
}

public ServerSideEncryptionCustomerKey ssec() {
return ssec;
}
private void validateSize(long objectSize) {
if (offset != null && offset >= objectSize) {
throwException(objectSize, offset, "offset");
}

public void validateSse(HttpUrl url) {
checkSse(ssec, url);
}
if (length != null) {
if (length > objectSize) {
throwException(objectSize, length, "length");
}

public static Builder builder() {
return new Builder();
if (offset + length > objectSize) {
throwException(objectSize, offset + length, "compose size");
}
}
}

/** Constructs header. */
public void buildHeaders(long objectSize, String etag) throws IllegalArgumentException {
public void buildHeaders(long objectSize, String etag) {
validateSize(objectSize);
Multimap<String, String> headers = HashMultimap.create();
headers.put("x-amz-copy-source", S3Escaper.encodePath(bucketName + "/" + objectName));
headers.put("x-amz-copy-source-if-match", etag);

if (extraHeaders() != null) {
headers.putAll(extraHeaders());
}
this.objectSize = Long.valueOf(objectSize);

if (matchETag != null) {
headers.put("x-amz-copy-source-if-match", matchETag);
String copySource = S3Escaper.encodePath(bucketName + "/" + objectName);
if (versionId != null) {
copySource += "?versionId=" + S3Escaper.encode(versionId);
}

if (ssec != null) {
headers.putAll(Multimaps.forMap(ssec.copySourceHeaders()));
}
Multimap<String, String> headers = HashMultimap.create();
headers.put("x-amz-copy-source", copySource);
headers.put("x-amz-copy-source-if-match", (matchETag != null) ? matchETag : etag);

if (notMatchETag != null) {
headers.put("x-amz-copy-source-if-none-match", notMatchETag);
Expand All @@ -115,90 +109,36 @@ public void buildHeaders(long objectSize, String etag) throws IllegalArgumentExc
unmodifiedSince.format(Time.HTTP_HEADER_DATE_FORMAT));
}

this.objectSize = objectSize;
this.headers = headers;
}

private void validateSize(long objectSize) throws IllegalArgumentException {
if (offset != null && offset >= objectSize) {
throw new IllegalArgumentException(
"source "
+ bucketName
+ "/"
+ objectName
+ ": offset "
+ offset
+ " is beyond object size "
+ objectSize);
if (ssec != null) {
headers.putAll(Multimaps.forMap(ssec.copySourceHeaders()));
}

if (length != null) {
if (length > objectSize) {
throw new IllegalArgumentException(
"source "
+ bucketName
+ "/"
+ objectName
+ ": length "
+ length
+ " is beyond object size "
+ objectSize);
}

if (offset + length > objectSize) {
throw new IllegalArgumentException(
"source "
+ bucketName
+ "/"
+ objectName
+ ": compose size "
+ (offset + length)
+ " is beyond object size "
+ objectSize);
}
}
this.headers = Multimaps.unmodifiableMultimap(headers);
}

/** Argument builder of {@link ComposeSource}. */
public static final class Builder extends ObjectVersionArgs.Builder<Builder, ComposeSource> {

public Builder offset(long offset) {
validateNullOrPositive(offset, "offset");
operations.add(args -> args.offset = offset);
return this;
}

public Builder length(long length) {
validateNullOrPositive(length, "length");
operations.add(args -> args.length = length);
return this;
public long objectSize() throws InternalException {
if (this.objectSize == null) {
throw new InternalException(
"buildHeaders(long objectSize, String etag) must be called prior this method invocation");
}

public Builder ssec(ServerSideEncryptionCustomerKey ssec) {
operations.add(args -> args.ssec = ssec);
return this;
}

public Builder matchETag(String etag) {
validateNullOrNotEmptyString(etag, "etag");
operations.add(args -> args.matchETag = etag);
return this;
}
return this.objectSize;
}

public Builder notMatchETag(String etag) {
validateNullOrNotEmptyString(etag, "etag");
operations.add(args -> args.notMatchETag = etag);
return this;
public Multimap<String, String> headers() throws InternalException {
if (this.headers == null) {
throw new InternalException(
"buildHeaders(long objectSize, String etag) must be called prior this method invocation");
}

public Builder modifiedSince(ZonedDateTime modifiedTime) {
operations.add(args -> args.modifiedSince = modifiedTime);
return this;
}
return this.headers;
}

public Builder unmodifiedSince(ZonedDateTime unmodifiedTime) {
operations.add(args -> args.unmodifiedSince = unmodifiedTime);
return this;
}
public static Builder builder() {
return new Builder();
}

/** Argument builder of {@link ComposeSource}. */
public static final class Builder
extends ObjectConditionalReadArgs.Builder<Builder, ComposeSource> {}
}
Loading

0 comments on commit 5f4a9dd

Please sign in to comment.