-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/concurrency tests #6
Changes from all commits
66fa8b6
1ff8078
9138f96
daf2c25
a477769
04d8e9c
8116216
22315e5
79d4221
65adfdf
41b78f4
f741adf
e29f313
4c0754b
52f8728
c6777a6
d5189d3
8a10dc2
b9f9eed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,20 +11,25 @@ project.ext { | |
developer = "Frank Wisniewski" | ||
developerId = "frankwis" | ||
developerMail = "[email protected]" | ||
}; | ||
} | ||
|
||
ext { | ||
slf4jVersion = "1.7.18" | ||
} | ||
|
||
dependencies { | ||
|
||
compile([ | ||
"org.apache.avro:avro:1.8.0", | ||
"org.slf4j:slf4j-api:1.7.18" | ||
"org.apache.avro:avro:1.8.1", | ||
"org.slf4j:slf4j-api:$slf4jVersion", | ||
"com.google.code.findbugs:jsr305:3.0.1" | ||
]) | ||
|
||
testCompile([ | ||
"junit:junit:4.12", | ||
"org.hamcrest:hamcrest-all:1.3", | ||
"com.google.guava:guava-testlib:19.0", | ||
"org.slf4j:slf4j-simple:1.7.18" | ||
"org.slf4j:slf4j-simple:$slf4jVersion" | ||
]) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,40 @@ | ||
package io.datanerds.avropatch; | ||
|
||
import io.datanerds.avropatch.value.DefaultSchema; | ||
import io.datanerds.avropatch.operation.Operation; | ||
import org.apache.avro.Schema; | ||
import org.apache.avro.io.*; | ||
import org.apache.avro.reflect.ReflectData; | ||
import org.apache.avro.reflect.ReflectDatumReader; | ||
import org.apache.avro.reflect.ReflectDatumWriter; | ||
|
||
import java.io.ByteArrayOutputStream; | ||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
|
||
import org.apache.avro.reflect.AvroSchema; | ||
import java.util.*; | ||
|
||
/** | ||
* This class represents a JSON PATCH operation holding a sequence of operations to apply to a given object. | ||
* | ||
* @see <a href="https://tools.ietf.org/html/rfc6902">https://tools.ietf.org/html/rfc6902</a> | ||
*/ | ||
public class Patch { | ||
|
||
public static final Schema SCHEMA = ReflectData.get().getSchema(Patch.class); | ||
private static final DatumWriter<Patch> writer = new ReflectDatumWriter<>(SCHEMA); | ||
@AvroSchema(DefaultSchema.HEADERS) | ||
private final Map<String, ?> headers; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't headers be a multimap? |
||
private final List<Operation> operations; | ||
|
||
public Patch() { | ||
operations = new ArrayList<>(); | ||
@SuppressWarnings("unused") // no-arg constructor needed by Avro | ||
private Patch() { | ||
this(new ArrayList<>(), new HashMap<>()); | ||
} | ||
|
||
public Patch(List<Operation> operations) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Always a good idea to declare method arguments as final (especially in case of collections) which makes it explicit to the calling function that the list would not be modified in the called method. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Disagree. The Agree though on parameter declarations being implicitly |
||
this.operations = new ArrayList<>(operations); | ||
this(operations, Collections.EMPTY_MAP); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not call the two-argument constructor here? This would follow the same principle as the no-argument constructor... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct :-) |
||
|
||
public boolean addOperation(Operation operation) { | ||
return operations.add(operation); | ||
public Patch(List<Operation> operations, Map<String, ?> headers) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we do a sanity check that the value instances are of a supported type? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I created issue #10 for this feature |
||
this.operations = new ArrayList<>(operations); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following the pattern above, |
||
this.headers = new HashMap<>(headers); | ||
} | ||
|
||
public List<Operation> getOperations() { | ||
return Collections.unmodifiableList(operations); | ||
} | ||
|
||
public byte[] toBytes() throws IOException { | ||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); | ||
Encoder binaryEncoder = EncoderFactory.get().directBinaryEncoder(outputStream, null); | ||
|
||
writer.write(this, binaryEncoder); | ||
return outputStream.toByteArray(); | ||
} | ||
|
||
public static Patch of(byte[] bytes) throws IOException { | ||
DatumReader<Patch> reader = new ReflectDatumReader<>(SCHEMA); | ||
return reader.read(null, DecoderFactory.get().binaryDecoder(bytes, null)); | ||
public Map<String, ?> getHeaders() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a concurrency issue with the map as long as the map is not open to modification. It might not be the case right now but leaves the code open to errors in case there is a write interface exposed to the map erroneously in the future. Would suggest initializing the map as immutable during initialization in the constructor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the map should not be open to modifications There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed that it is not open to modifications to external modifiers but from a code extensibility perspective, if there is a method in this class which tries to modify this map, extra care would need to be taken to ensure that the map is made immutable then. By making it immutable now, we can future proof it so that we don't run the risk of somebody not making it immutable/thread safe while adding code modifying it in future. |
||
return Collections.unmodifiableMap(headers); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
package io.datanerds.avropatch.exception; | ||
|
||
public class AvroPatchException extends RuntimeException { | ||
public abstract class AvroPatchException extends RuntimeException { | ||
|
||
public AvroPatchException(String message) { | ||
AvroPatchException(String message) { | ||
super(message); | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package io.datanerds.avropatch.exception; | ||
|
||
public class InvalidReferenceTokenException extends AvroPatchException { | ||
|
||
public InvalidReferenceTokenException(String message) { | ||
super(message); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,57 @@ | ||
package io.datanerds.avropatch.operation; | ||
|
||
import io.datanerds.avropatch.exception.InvalidPathException; | ||
import org.apache.avro.reflect.AvroSchema; | ||
import io.datanerds.avropatch.exception.InvalidReferenceTokenException; | ||
|
||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import java.util.function.Function; | ||
import java.util.regex.Pattern; | ||
|
||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
/** | ||
* This class represents a JavaScript Object Notation (JSON) Pointer object as defined in RFC 6901. {@link Path} | ||
* instances identify a specific value within a JSON document by maintaining a sequence of zero or more reference | ||
* tokens, each prefixed by a {@code '/'} character. | ||
* <p> | ||
* Assume the following JSON document: | ||
* <blockquote><pre> | ||
* { | ||
* "employees" : [ | ||
* { | ||
* "firstName" : "John", | ||
* "lastName" : "Doe", | ||
* "age" : 42, | ||
* "address" : { | ||
* "street" : "1234 Main Street", | ||
* "city" : "San Francisco", | ||
* "zip" : "94114" | ||
* } | ||
* }, | ||
* { | ||
* "firstName" : "Jane", | ||
* "lastName" : "Doe", | ||
* "age" : 42, | ||
* "address" : { | ||
* "street" : "4321 Main Street", | ||
* "city" : "New York", | ||
* "zip" : "10001" | ||
* } | ||
* } | ||
* ] | ||
* } | ||
* </pre></blockquote> | ||
* <p> | ||
* The JSON {@link Path} for addressing value "John" would be {@code "/employees/0/firstName"}, for the Jane's entire | ||
* record {@code "/employees/1"} and to specify the street within the JSON blob representing her address | ||
* {@code "/employees/1/address"}. | ||
* @see <a href="https://tools.ietf.org/html/rfc6901">RFC 6901: JavaScript Object Notation (JSON) Pointer</a> | ||
*/ | ||
public class Path { | ||
|
||
public static final String SLASH = "/"; | ||
public static final Path ROOT = new Path(); | ||
|
||
private static final Pattern VALID_PATTERN = Pattern.compile("[0-9a-zA-Z][0-9a-zA-Z_-]*"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to give examples of valid paths somewhere in the JavaDoc? |
||
|
||
@AvroSchema("{\"type\": \"array\", \"items\": \"string\"}") | ||
private final List<String> parts; | ||
|
||
private Path() { | ||
|
@@ -27,12 +62,42 @@ private Path(List<String> parts) { | |
this.parts = Collections.unmodifiableList(parts); | ||
} | ||
|
||
public static Path of(String... parts) { | ||
List<String> paths = Arrays.asList(parts); | ||
paths.forEach(Path::validateSubPath); | ||
/** | ||
* Convenient method for retrieving root path {@code "/"}. | ||
* @return {@link #ROOT} | ||
*/ | ||
public static Path of() { | ||
return ROOT; | ||
} | ||
|
||
/** | ||
* Constructs a {@link Path} object for given reference tokens. | ||
* @param referenceTokens {@link String} representation of all reference tokens for {@link Path} without prefix | ||
* {@code "/"} | ||
* @return JSON path assembled via given reference tokens | ||
*/ | ||
public static Path of(String... referenceTokens) { | ||
return new Path(validatedReferenceTokens(referenceTokens).collect(Collectors.toList())); | ||
} | ||
|
||
/** | ||
* Constructs a {@link Path} object by concatenating given paths. | ||
* @param subpaths paths to be concatenated for new {@link Path} object | ||
* @return JSON path assembled by concatenating given paths | ||
*/ | ||
public static Path of(Path... subpaths) { | ||
List<String> paths = Stream.of(subpaths) | ||
.flatMap(Path.partsStream()) | ||
.collect(Collectors.toList()); | ||
return new Path(paths); | ||
} | ||
|
||
/** | ||
* Parses a {@link String} representation of a JSON path (e.g. {@code "/employees/0/firstName"}, | ||
* {@code "/"} or {@code "/employees/1/address"}). | ||
* @param path valid {@link String} representation of a JSON path | ||
* @return JSON path instance | ||
*/ | ||
public static Path parse(String path) { | ||
verifyParsable(path); | ||
if (SLASH.equals(path)) { | ||
|
@@ -41,11 +106,28 @@ public static Path parse(String path) { | |
return of(path.substring(1).split(SLASH)); | ||
} | ||
|
||
private static void verifyParsable(String path) { | ||
Objects.nonNull(path); | ||
if (!path.startsWith(SLASH)) { | ||
throw new InvalidPathException("JSON Path has to start with a slash."); | ||
} | ||
/** | ||
* Constructs a new {@link Path} object by appending the given subpaths to the JSON path represented by this object. | ||
* @param subpaths {@link Path} objects to be appended | ||
* @return concatenated {@link Path} | ||
*/ | ||
public Path append(Path... subpaths) { | ||
List<String> paths = Stream.concat(this.parts().stream(), Stream.of(subpaths).flatMap(partsStream())) | ||
.collect(Collectors.toList()); | ||
return new Path(paths); | ||
} | ||
|
||
/** | ||
* Constructs a new {@link Path} object by appending the JSON path specified by the given reference tokens to the | ||
* path represented by this instance. | ||
* @param referenceTokens {@link String} representation of all reference tokens for {@link Path} without prefix | ||
* {@code "/"} | ||
* @return concatenated {@link Path} | ||
*/ | ||
public Path append(String... referenceTokens) { | ||
List<String> paths = Stream.concat(this.parts().stream(), validatedReferenceTokens(referenceTokens)) | ||
.collect(Collectors.toList()); | ||
return new Path(paths); | ||
} | ||
|
||
public Path parent() { | ||
|
@@ -59,9 +141,25 @@ public List<String> parts() { | |
return parts; | ||
} | ||
|
||
private static void validateSubPath(String path) { | ||
private static Function<Path, Stream<String>> partsStream() { | ||
return path -> path.parts().stream(); | ||
} | ||
|
||
private static Stream<String> validatedReferenceTokens(String[] referenceTokens) { | ||
return Stream.of(referenceTokens).map(Path::validateReferenceToken); | ||
} | ||
|
||
private static String validateReferenceToken(String path) { | ||
if (!VALID_PATTERN.matcher(path).matches()) { | ||
throw new InvalidPathException(String.format("%s is not a valid JSON path", path)); | ||
throw new InvalidReferenceTokenException(String.format("%s is not a valid JSON path", path)); | ||
} | ||
return path; | ||
} | ||
|
||
private static void verifyParsable(String path) { | ||
Objects.nonNull(path); | ||
if (!path.startsWith(SLASH)) { | ||
throw new InvalidReferenceTokenException("JSON Path has to start with a slash."); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
slf4j version should become a variable since it is also used in testCompile