Skip to content

Commit 29a945f

Browse files
authored
Make Transformers serializable (#4)
* add some brittle serialization logic * add warning * Remove brittle code * update serialization logic * remove main method * update changelog * update changelog * remove warnings * add caching and more unit tests * use released version of accent4j 1.4.0
1 parent 198d254 commit 29a945f

File tree

9 files changed

+672
-5
lines changed

9 files changed

+672
-5
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ hs_err_pid*
1818
.project
1919
.settings
2020
build/
21+
22+
.DS_Store

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
#### Version 1.3.0 (TBD)
4+
* Added the `ScriptedTransformer` framework. `ScriptedTransformer` is abstract class that can be extended to provide transformation logic via a script that is compatible with the Java script engine platform. Right now, javascript is the supported language for writing transformer scripts.
5+
* Added support for Transformer serialization. The rules of serialization are:
6+
* All built-in transformers provided in the `Transformers` factory class are serializable.
7+
* `ScriptedTransformer`s are serializable.
8+
* A `CompositeTransformer` can be serialized if all of the composed transformers can be serailized.
9+
* Custom transformers that require serialization should be implemented using the `ScriptedTransformer` framework.
10+
* Added `Transformer#serialize` and `Transformer#deserialize` static methods.
11+
12+
313
#### Version 1.2.0 (November 2, 2018)
414
* Deprecated and renamed a few `Transformer` factories for better consistency
515
* `keyCaseFormat` is deprecated in favor of `keyConditionalConvertCaseFormat` or `keyEnsureCaseFormat`

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ repositories {
6969
}
7070

7171
dependencies {
72-
compile group: 'com.cinchapi', name: 'accent4j', version: '1.3.0', changing:true
72+
compile group: 'com.cinchapi', name: 'accent4j', version: '1.4.0', changing:true
7373
compile group: 'com.cinchapi', name: 'concourse-driver-java', version: concourseVersion, changing:true // needed for util classes...
7474
compile group: 'com.google.code.findbugs', name: 'jsr305', version:'2.0.1'
7575
compile group: 'com.google.guava', name:'guava', version:'25.1-jre'

src/main/java/com/cinchapi/etl/CompositeTransformer.java

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,25 @@
1515
*/
1616
package com.cinchapi.etl;
1717

18+
import java.io.IOException;
19+
import java.io.ObjectInputStream;
20+
import java.io.ObjectOutputStream;
21+
import java.io.Serializable;
22+
import java.nio.ByteBuffer;
1823
import java.util.List;
1924
import java.util.Map;
2025
import java.util.Map.Entry;
2126
import javax.annotation.Nullable;
22-
27+
import com.cinchapi.common.base.CheckedExceptions;
2328
import com.cinchapi.common.collect.AnyMaps;
2429
import com.cinchapi.common.collect.MergeStrategies;
30+
import com.cinchapi.common.reflect.Reflection;
31+
import com.cinchapi.concourse.util.ByteBuffers;
2532
import com.google.common.base.MoreObjects;
2633
import com.google.common.base.Preconditions;
2734
import com.google.common.collect.ImmutableList;
2835
import com.google.common.collect.ImmutableMap;
36+
import com.google.common.collect.Lists;
2937
import com.google.common.collect.Maps;
3038

3139
/**
@@ -34,10 +42,18 @@
3442
* <p>
3543
* Each of the composing Transformers is applied in declaration order.
3644
* </p>
45+
* <h1>Serializability</h1>
46+
* <p>
47+
* A {@link CompositedTransformer} can be
48+
* {@link Transformer#serialize(Transformer) serialized} if an only if all of
49+
* the transformers being composed can be serialized.
50+
* </p>
3751
*
3852
* @author Jeff Nelson
3953
*/
40-
class CompositeTransformer implements Transformer {
54+
class CompositeTransformer implements Transformer, Serializable {
55+
56+
private static final long serialVersionUID = 1759080269596158582L;
4157

4258
/**
4359
* Apply the {@code transformer} to each key/value mapping within the
@@ -139,4 +155,45 @@ public Map<String, Object> transform(String key, Object value) {
139155
return transformed;
140156
}
141157

158+
/**
159+
* Deserialize this object from the {@code in} stream.
160+
*
161+
* @param in
162+
* @throws IOException
163+
* @throws ClassNotFoundException
164+
*/
165+
private void readObject(ObjectInputStream in)
166+
throws IOException, ClassNotFoundException {
167+
Reflection.set("transformers", Lists.newArrayList(), this);
168+
int count = in.readInt();
169+
for (int i = 0; i < count; ++i) {
170+
int size = in.readInt();
171+
byte[] bytes = new byte[size];
172+
in.readFully(bytes);
173+
Transformer transformer = Transformer
174+
.deserialize(ByteBuffer.wrap(bytes));
175+
transformers.add(transformer);
176+
}
177+
}
178+
179+
/**
180+
* Serialize this object to the {@code out} stream.
181+
*
182+
* @param out
183+
* @throws IOException
184+
*/
185+
private void writeObject(ObjectOutputStream out) throws IOException {
186+
out.writeInt(transformers.size());
187+
transformers.forEach(transformer -> {
188+
try {
189+
ByteBuffer bytes = Transformer.serialize(transformer);
190+
out.writeInt(bytes.remaining());
191+
out.write(ByteBuffers.toByteArray(bytes));
192+
}
193+
catch (IOException e) {
194+
throw CheckedExceptions.wrapAsRuntimeException(e);
195+
}
196+
});
197+
}
198+
142199
}
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
/*
2+
* Copyright (c) 2013-2018 Cinchapi Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.cinchapi.etl;
17+
18+
import java.io.IOException;
19+
import java.io.ObjectInputStream;
20+
import java.io.ObjectOutputStream;
21+
import java.io.Serializable;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.Map;
24+
25+
import javax.script.Bindings;
26+
import javax.script.ScriptEngine;
27+
import javax.script.ScriptEngineManager;
28+
import javax.script.ScriptException;
29+
import javax.script.SimpleBindings;
30+
31+
import com.cinchapi.common.base.AnyStrings;
32+
import com.cinchapi.common.base.CheckedExceptions;
33+
import com.cinchapi.common.base.Verify;
34+
import com.cinchapi.common.collect.AnyMaps;
35+
import com.cinchapi.common.collect.Association;
36+
import com.cinchapi.common.reflect.Reflection;
37+
import com.google.common.collect.ImmutableMap;
38+
39+
/**
40+
* A {@link Transformer} that uses a {@link ScriptEngine} compatible script to
41+
* transform data.
42+
* <p>
43+
* The provided script has access to the key and value that are candidates for
44+
* transformation. The key is available via a variable named {@code key} and
45+
* the value is available via a variable named {@code value}.
46+
* </p>
47+
* <p>
48+
* The last line of the script determines the return value of the
49+
* transformation. If an Object or Map-like value is returned from the script,
50+
* the same is returned from the {@link #transform(String, Object)} function.
51+
* Otherwise, scalar values are assumed to be transformations to the original
52+
* value. In those instances, the original key is preserved and the original
53+
* value is replaced by the script's returned value.
54+
* </p>
55+
*
56+
* @author Jeff Nelson
57+
*/
58+
public class ScriptedTransformer implements Transformer, Serializable {
59+
60+
private static final long serialVersionUID = 1596575180656202243L;
61+
62+
/**
63+
* Reference to the {@link ScriptEngineManager}.
64+
*/
65+
private static final ScriptEngineManager sem = new ScriptEngineManager();
66+
67+
/**
68+
* Return a builder that will create a {@link ScriptedTransformer} that uses
69+
* a javascript interpreter.
70+
*
71+
* @return the builder
72+
*/
73+
public static Builder usingJavascript() {
74+
return new JavascriptTransformerBuilder();
75+
}
76+
77+
/**
78+
* The {@link ScriptEngine} that handles {@link #script} evaluation.
79+
*/
80+
private transient final ScriptEngine engine;
81+
82+
private final String engineName;
83+
84+
/**
85+
* The script that is used for transformation.
86+
*/
87+
private final String script;
88+
89+
/**
90+
* Construct a new instance.
91+
*
92+
* @param engine
93+
* @param script
94+
*/
95+
private ScriptedTransformer(String engine, String script) {
96+
this.engineName = engine;
97+
this.script = script;
98+
this.engine = sem.getEngineByName(engine);
99+
Verify.that(engine != null, "Invalid script engine {}", engine);
100+
}
101+
102+
@SuppressWarnings("unchecked")
103+
@Override
104+
public Map<String, Object> transform(String key, Object value) {
105+
Bindings bindings = new SimpleBindings(
106+
Association.of(ImmutableMap.of("key", key, "value", value)));
107+
try {
108+
Object result = engine.eval(script, bindings);
109+
if(result instanceof Map) {
110+
// The #result is probably actually an instance of JsObject
111+
// which has a toString of [Object object]. In order to get the
112+
// object's properties into a Java friendly Map format we must
113+
// make a copy.
114+
boolean isArray;
115+
try {
116+
isArray = Reflection.call(result, "isArray");
117+
}
118+
catch (Exception e) {
119+
isArray = false;
120+
}
121+
result = isArray ? ((Map<String, Object>) result).values()
122+
: ImmutableMap.copyOf((Map<String, Object>) result);
123+
}
124+
if(result instanceof Map) {
125+
return (Map<String, Object>) result;
126+
}
127+
else {
128+
// If the script returns anything other than a Map-like object,
129+
// assume that the transformation only applies to the value and
130+
// preserve the original key.
131+
return AnyMaps.create(key, result);
132+
}
133+
}
134+
catch (ScriptException e) {
135+
throw CheckedExceptions.wrapAsRuntimeException(e);
136+
}
137+
}
138+
139+
/**
140+
* Deserialize this object from the {@code in} stream.
141+
*
142+
* @param in
143+
* @throws IOException
144+
* @throws ClassNotFoundException
145+
*/
146+
private void readObject(ObjectInputStream in)
147+
throws IOException, ClassNotFoundException {
148+
// Read script
149+
short scriptBytesLength = in.readShort();
150+
byte[] scriptBytes = new byte[scriptBytesLength];
151+
in.read(scriptBytes);
152+
String script = new String(scriptBytes, StandardCharsets.UTF_8);
153+
Reflection.set("script", script, this);
154+
155+
// Read engine
156+
short engineNameBytesLength = in.readShort();
157+
byte[] engineNameBytes = new byte[engineNameBytesLength];
158+
in.read(engineNameBytes);
159+
String engineName = new String(engineNameBytes, StandardCharsets.UTF_8);
160+
ScriptEngine engine = sem.getEngineByName(engineName);
161+
Reflection.set("engine", engine, this);
162+
}
163+
164+
/**
165+
* Serialize this object to the {@code out} stream.
166+
*
167+
* @param out
168+
* @throws IOException
169+
*/
170+
private void writeObject(ObjectOutputStream out) throws IOException {
171+
byte[] scriptBytes = script.getBytes(StandardCharsets.UTF_8);
172+
byte[] engineNameBytes = engineName.toString()
173+
.getBytes(StandardCharsets.UTF_8);
174+
out.writeShort(scriptBytes.length);
175+
out.write(scriptBytes);
176+
out.writeShort(engineNameBytes.length);
177+
out.write(engineNameBytes);
178+
}
179+
180+
/**
181+
* Base {@link Builder} for {@link ScriptedTransformer}s.
182+
*
183+
* @author Jeff Nelson
184+
*/
185+
public static abstract class Builder {
186+
187+
private final String engine;
188+
private final StringBuilder script = new StringBuilder();
189+
190+
/**
191+
* Construct a new instance.
192+
*
193+
* @param engine
194+
*/
195+
protected Builder(String engine) {
196+
this.engine = engine;
197+
}
198+
199+
/**
200+
* Build the transformer.
201+
*
202+
* @return the {@link ScriptedTransformer}
203+
*/
204+
public final ScriptedTransformer build() {
205+
return new ScriptedTransformer(engine, script.toString());
206+
}
207+
208+
/**
209+
* Define a variable within the script
210+
*
211+
* @param var
212+
* @param value
213+
* @return this
214+
*/
215+
public Builder define(String var, String value) {
216+
String line = doDefine(var, value);
217+
interpret(line);
218+
return this;
219+
}
220+
221+
/**
222+
* Add a line of logic to the script.
223+
*
224+
* @param line
225+
* @return this
226+
*/
227+
public Builder interpret(String line) {
228+
script.append(line).append(System.lineSeparator());
229+
return this;
230+
}
231+
232+
/**
233+
* Set {@code var} equal to {@code value} in the script context.
234+
*
235+
* @param var
236+
* @param value
237+
* @return this
238+
*/
239+
protected abstract String doDefine(String var, String value);
240+
241+
}
242+
243+
/**
244+
* A {@link Builder} for Javascript based {@link ScriptedTransformer}s.
245+
*
246+
* @author Jeff Nelson
247+
*/
248+
public static class JavascriptTransformerBuilder extends Builder {
249+
250+
/**
251+
* Construct a new instance.
252+
*/
253+
public JavascriptTransformerBuilder() {
254+
super("javascript");
255+
define("transformers", AnyStrings.format("Java.type('{}')",
256+
Transformers.class.getName()));
257+
}
258+
259+
@Override
260+
protected String doDefine(String var, String value) {
261+
return AnyStrings.format("{} = {};", var, value);
262+
}
263+
264+
}
265+
266+
}

0 commit comments

Comments
 (0)